diff --git a/Geesemon.Web/Commands/CommandAttribute.cs b/Geesemon.Web/Commands/CommandAttribute.cs new file mode 100644 index 0000000..b290ea0 --- /dev/null +++ b/Geesemon.Web/Commands/CommandAttribute.cs @@ -0,0 +1,7 @@ +namespace Geesemon.Web.Commands; + +[AttributeUsage(AttributeTargets.Method)] +public class CommandAttribute(string command) : Attribute +{ + public string Command => command; +} diff --git a/Geesemon.Web/Commands/CommandContext.cs b/Geesemon.Web/Commands/CommandContext.cs new file mode 100644 index 0000000..edc99d9 --- /dev/null +++ b/Geesemon.Web/Commands/CommandContext.cs @@ -0,0 +1,6 @@ +namespace Geesemon.Web.Commands; + +public readonly record struct CommandContext( + string Message, + Guid FromId, + Guid ChatId); diff --git a/Geesemon.Web/Commands/CommandExecutor.cs b/Geesemon.Web/Commands/CommandExecutor.cs new file mode 100644 index 0000000..36b8903 --- /dev/null +++ b/Geesemon.Web/Commands/CommandExecutor.cs @@ -0,0 +1,52 @@ +using System.Reflection; + +namespace Geesemon.Web.Commands; + +public class CommandExecutor +{ + readonly List commandInfos = []; + + public CommandExecutor(IEnumerable commandModules) + { + foreach (var module in commandModules) + { + var commandInfos = module + .GetType() + .GetMethods() + .Select(commandMethod => + { + var command = commandMethod.GetCustomAttributes(inherit: false).FirstOrDefault()?.Command; + return new CommandInfo(module, commandMethod, command); + }) + .Where(i => !string.IsNullOrWhiteSpace(i.Command)); + + this.commandInfos.AddRange(commandInfos); + } + } + + public async Task Execute(CommandContext context) + { + foreach (var commandInfo in commandInfos) + { + if (context.Message == commandInfo.Command) + { + await (Task)commandInfo.CommandMethod.Invoke(commandInfo.Module, [context]); + return; + } + + var command = commandInfo.Command + " "; + + if (context.Message.StartsWith(command)) + { + var message = context.Message[command.Length..]; + + context = context with { Message = message }; + + await (Task)commandInfo.CommandMethod.Invoke(commandInfo.Module, [context]); + return; + } + } + } +} + +readonly record struct CommandInfo(ICommandModule Module, MethodInfo CommandMethod, string Command); diff --git a/Geesemon.Web/Commands/ICommandModule.cs b/Geesemon.Web/Commands/ICommandModule.cs new file mode 100644 index 0000000..f417d13 --- /dev/null +++ b/Geesemon.Web/Commands/ICommandModule.cs @@ -0,0 +1,5 @@ +namespace Geesemon.Web.Commands; + +public interface ICommandModule +{ +} diff --git a/Geesemon.Web/Commands/Modules/ChatGptModule.cs b/Geesemon.Web/Commands/Modules/ChatGptModule.cs new file mode 100644 index 0000000..571a965 --- /dev/null +++ b/Geesemon.Web/Commands/Modules/ChatGptModule.cs @@ -0,0 +1,49 @@ +using Geesemon.DataAccess.Dapper.Providers; +using Geesemon.Model.Models; +using Geesemon.Web.Services.MessageSubscription; +using Geesemon.Web.Utils.SettingsAccess; + +using OpenAI_API; + +namespace Geesemon.Web.Commands.Modules; + +public class ChatGptModule( + ISettingsProvider settingsProvider, + MessageProvider messageProvider, + IMessageActionSubscriptionService messageActionSubscriptionService) + : ICommandModule +{ + readonly MessageProvider messageProvider = messageProvider; + readonly IMessageActionSubscriptionService messageActionSubscriptionService = messageActionSubscriptionService; + readonly OpenAIAPI chatGpt = new OpenAIAPI(settingsProvider.GetChatGptApiKey()); + + [Command("/ai")] + public async Task Ai(CommandContext context) + { + var conversation = chatGpt.Chat.CreateConversation(); + + conversation.AppendUserInput(context.Message); + + Message? message = null; + await foreach (var messagePart in conversation.StreamResponseEnumerableFromChatbotAsync()) + { + if (message == null) + { + message = new Message + { + ChatId = context.ChatId, + Text = messagePart, + FromId = context.FromId, + }; + + message = await messageProvider.CreateAsync(message); + messageActionSubscriptionService.Notify(message, MessageActionKind.Create); + continue; + } + + message.Text += messagePart; + message = await messageProvider.UpdateAsync(message); + messageActionSubscriptionService.Notify(message, MessageActionKind.Update); + } + } +} diff --git a/Geesemon.Web/Extensions/ServiceCollectionExtensions.cs b/Geesemon.Web/Extensions/ServiceCollectionExtensions.cs index 547eddf..b508c01 100644 --- a/Geesemon.Web/Extensions/ServiceCollectionExtensions.cs +++ b/Geesemon.Web/Extensions/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Geesemon.Migrations; using Geesemon.Model.Enums; +using Geesemon.Web.Commands; using Geesemon.Web.Geesetext; using Geesemon.Web.GraphQL; using Geesemon.Web.GraphQL.Auth; @@ -108,5 +109,21 @@ public static IServiceCollection AddServices(this IServiceCollection services, I return services; } + + public static IServiceCollection AddCommandModules(this IServiceCollection services) + { + var commandModuleType = typeof(ICommandModule); + + var commandModules = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .Where(t => t.GetInterfaces().Contains(commandModuleType)); + + foreach (var commandModule in commandModules) + services.AddSingleton(commandModuleType, commandModule); + + services.AddSingleton(); + + return services; + } } } diff --git a/Geesemon.Web/Geesemon.Web.csproj b/Geesemon.Web/Geesemon.Web.csproj index 6a148b3..dce1805 100644 --- a/Geesemon.Web/Geesemon.Web.csproj +++ b/Geesemon.Web/Geesemon.Web.csproj @@ -49,6 +49,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs b/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs index 8a94233..ca558e1 100644 --- a/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs +++ b/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs @@ -4,6 +4,7 @@ using Geesemon.DataAccess.Managers; using Geesemon.Model.Common; using Geesemon.Model.Models; +using Geesemon.Web.Commands; using Geesemon.Web.GraphQL.Auth; using Geesemon.Web.GraphQL.Types; using Geesemon.Web.Services.FileManagers; @@ -24,7 +25,8 @@ public MessageMutation( ReadMessagesManager readMessagesManager, IValidator sentMessageInputValidator, IValidator deleteMessageInputValidator, - IFileManagerService fileManagerService + IFileManagerService fileManagerService, + CommandExecutor commandExecutor ) { Field>, IEnumerable>() @@ -122,8 +124,9 @@ IFileManagerService fileManagerService createdMessages.Add(createdMessage); } - return createdMessages; + await commandExecutor.Execute(new(sentMessageInput.Text, currentUserId, chat.Id)); + return createdMessages; }) .AuthorizeWith(AuthPolicies.Authenticated); diff --git a/Geesemon.Web/Program.cs b/Geesemon.Web/Program.cs index c1de3b5..0ce88d3 100644 --- a/Geesemon.Web/Program.cs +++ b/Geesemon.Web/Program.cs @@ -3,10 +3,12 @@ using Geesemon.Migrations.Extensions; using Geesemon.Web.Extensions; using Geesemon.Web.GraphQL; + using Microsoft.Extensions.FileProviders; var builder = WebApplication.CreateBuilder(args); builder.Services.AddServices(builder.Configuration); +builder.Services.AddCommandModules(); builder.Services.AddMigrationServices(builder.Configuration); var connectionString = builder.Configuration.GetValue("ConnectionString"); diff --git a/Geesemon.Web/Utils/SettingsAccess/AppSettingsProvider.cs b/Geesemon.Web/Utils/SettingsAccess/AppSettingsProvider.cs index eab485a..6ca0e64 100644 --- a/Geesemon.Web/Utils/SettingsAccess/AppSettingsProvider.cs +++ b/Geesemon.Web/Utils/SettingsAccess/AppSettingsProvider.cs @@ -43,5 +43,10 @@ public FileProvider GetFileProvider() { return configuration.GetValue("FileProvider"); } + + public string GetChatGptApiKey() + { + return configuration.GetValue("ChatGptApiKey"); + } } } diff --git a/Geesemon.Web/Utils/SettingsAccess/EnvironmentSettingsProvider.cs b/Geesemon.Web/Utils/SettingsAccess/EnvironmentSettingsProvider.cs index f1e7c68..4050685 100644 --- a/Geesemon.Web/Utils/SettingsAccess/EnvironmentSettingsProvider.cs +++ b/Geesemon.Web/Utils/SettingsAccess/EnvironmentSettingsProvider.cs @@ -36,5 +36,10 @@ public FileProvider GetFileProvider() { return Enum.Parse(Environment.GetEnvironmentVariable("FileProvider")); } + + public string GetChatGptApiKey() + { + return Environment.GetEnvironmentVariable("ChatGptApiKey"); + } } } diff --git a/Geesemon.Web/Utils/SettingsAccess/ISettingsProvider.cs b/Geesemon.Web/Utils/SettingsAccess/ISettingsProvider.cs index e878ef4..ac1f014 100644 --- a/Geesemon.Web/Utils/SettingsAccess/ISettingsProvider.cs +++ b/Geesemon.Web/Utils/SettingsAccess/ISettingsProvider.cs @@ -15,5 +15,7 @@ public interface ISettingsProvider string GetBlobConnectionString(); FileProvider GetFileProvider(); + + string GetChatGptApiKey(); } } \ No newline at end of file