From ce95cc637710d6a9e20092697d54bd7fb293fe3c Mon Sep 17 00:00:00 2001 From: Vitalii Vaskivskyi Date: Mon, 30 Sep 2024 11:23:10 +0300 Subject: [PATCH 1/5] add ai command --- Geesemon.Web/Geesemon.Web.csproj | 1 + .../GraphQL/Mutations/MessageMutation.cs | 32 +++++++++++++++++-- .../SettingsAccess/AppSettingsProvider.cs | 5 +++ .../EnvironmentSettingsProvider.cs | 5 +++ .../Utils/SettingsAccess/ISettingsProvider.cs | 2 ++ 5 files changed, 43 insertions(+), 2 deletions(-) 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..980dd0f 100644 --- a/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs +++ b/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs @@ -8,10 +8,13 @@ using Geesemon.Web.GraphQL.Types; using Geesemon.Web.Services.FileManagers; using Geesemon.Web.Services.MessageSubscription; +using Geesemon.Web.Utils.SettingsAccess; using GraphQL; using GraphQL.Types; +using System.Text.RegularExpressions; + namespace Geesemon.Web.GraphQL.Mutations { public class MessageMutation : ObjectGraphType @@ -24,7 +27,8 @@ public MessageMutation( ReadMessagesManager readMessagesManager, IValidator sentMessageInputValidator, IValidator deleteMessageInputValidator, - IFileManagerService fileManagerService + IFileManagerService fileManagerService, + ISettingsProvider settingsProvider ) { Field>, IEnumerable>() @@ -122,8 +126,32 @@ IFileManagerService fileManagerService createdMessages.Add(createdMessage); } - return createdMessages; + var match = Regex.Match(sentMessageInput.Text, @"^\/ai (.+)"); + if (match.Success) + { + var apiKey = settingsProvider.GetChatGptApiKey(); + var api = new OpenAI_API.OpenAIAPI(apiKey); + + var chatGpt = api.Chat.CreateConversation(); + chatGpt.Model = OpenAI_API.Models.Model.ChatGPTTurbo; + + var command = match.Groups[1].Value; + chatGpt.AppendUserInput(command); + + var chatGptResponse = await chatGpt.GetResponseFromChatbotAsync(); + + var newMessage = new Message + { + ChatId = chat.Id, + Text = chatGptResponse, + FromId = currentUserId, + }; + var createdMessage = await messageProvider.CreateAsync(newMessage); + messageActionSubscriptionService.Notify(createdMessage, MessageActionKind.Create); + createdMessages.Add(createdMessage); + } + return createdMessages; }) .AuthorizeWith(AuthPolicies.Authenticated); 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 From 3d70b5430e6a5cb94d009690cfdc9163df2f05bd Mon Sep 17 00:00:00 2001 From: Vitalii Vaskivskyi Date: Mon, 30 Sep 2024 12:52:14 +0300 Subject: [PATCH 2/5] move command in separate module --- Geesemon.Web/Commands/CommandAttribute.cs | 7 +++ Geesemon.Web/Commands/CommandContext.cs | 6 +++ Geesemon.Web/Commands/CommandExecutor.cs | 46 +++++++++++++++++++ .../Commands/CommandModuleAttribute.cs | 6 +++ Geesemon.Web/Commands/ICommandModule.cs | 5 ++ .../Commands/Modules/ChatGptModule.cs | 38 +++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 17 +++++++ .../GraphQL/Mutations/MessageMutation.cs | 31 ++----------- Geesemon.Web/Program.cs | 2 + 9 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 Geesemon.Web/Commands/CommandAttribute.cs create mode 100644 Geesemon.Web/Commands/CommandContext.cs create mode 100644 Geesemon.Web/Commands/CommandExecutor.cs create mode 100644 Geesemon.Web/Commands/CommandModuleAttribute.cs create mode 100644 Geesemon.Web/Commands/ICommandModule.cs create mode 100644 Geesemon.Web/Commands/Modules/ChatGptModule.cs 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..de74831 --- /dev/null +++ b/Geesemon.Web/Commands/CommandExecutor.cs @@ -0,0 +1,46 @@ +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(Context context) + { + foreach (var commandInfo in commandInfos) + { + var command = commandInfo.Command + " "; + + if (context.Message.StartsWith(command)) + { + var message = context.Message[command.Length..]; + + var commandContext = new CommandContext(message, context.FromId, context.ChatId); + await (Task)commandInfo.CommandMethod.Invoke(commandInfo.Module, [commandContext]); + } + } + } + + public readonly record struct Context(string Message, Guid FromId, Guid ChatId); +} + +readonly record struct CommandInfo(ICommandModule Module, MethodInfo CommandMethod, string Command); diff --git a/Geesemon.Web/Commands/CommandModuleAttribute.cs b/Geesemon.Web/Commands/CommandModuleAttribute.cs new file mode 100644 index 0000000..ff8a3a0 --- /dev/null +++ b/Geesemon.Web/Commands/CommandModuleAttribute.cs @@ -0,0 +1,6 @@ +namespace Geesemon.Web.Commands; + +[AttributeUsage(AttributeTargets.Class)] +public class CommandModuleAttribute : Attribute +{ +} 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..38d3b85 --- /dev/null +++ b/Geesemon.Web/Commands/Modules/ChatGptModule.cs @@ -0,0 +1,38 @@ +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); + + var chatGptResponse = await conversation.GetResponseFromChatbotAsync(); + + var newMessage = new Message + { + ChatId = context.ChatId, + Text = chatGptResponse, + FromId = context.FromId, + }; + + var createdMessage = await messageProvider.CreateAsync(newMessage); + messageActionSubscriptionService.Notify(createdMessage, MessageActionKind.Create); + } +} 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/GraphQL/Mutations/MessageMutation.cs b/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs index 980dd0f..ca558e1 100644 --- a/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs +++ b/Geesemon.Web/GraphQL/Mutations/MessageMutation.cs @@ -4,17 +4,15 @@ 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; using Geesemon.Web.Services.MessageSubscription; -using Geesemon.Web.Utils.SettingsAccess; using GraphQL; using GraphQL.Types; -using System.Text.RegularExpressions; - namespace Geesemon.Web.GraphQL.Mutations { public class MessageMutation : ObjectGraphType @@ -28,7 +26,7 @@ public MessageMutation( IValidator sentMessageInputValidator, IValidator deleteMessageInputValidator, IFileManagerService fileManagerService, - ISettingsProvider settingsProvider + CommandExecutor commandExecutor ) { Field>, IEnumerable>() @@ -126,30 +124,7 @@ ISettingsProvider settingsProvider createdMessages.Add(createdMessage); } - var match = Regex.Match(sentMessageInput.Text, @"^\/ai (.+)"); - if (match.Success) - { - var apiKey = settingsProvider.GetChatGptApiKey(); - var api = new OpenAI_API.OpenAIAPI(apiKey); - - var chatGpt = api.Chat.CreateConversation(); - chatGpt.Model = OpenAI_API.Models.Model.ChatGPTTurbo; - - var command = match.Groups[1].Value; - chatGpt.AppendUserInput(command); - - var chatGptResponse = await chatGpt.GetResponseFromChatbotAsync(); - - var newMessage = new Message - { - ChatId = chat.Id, - Text = chatGptResponse, - FromId = currentUserId, - }; - var createdMessage = await messageProvider.CreateAsync(newMessage); - messageActionSubscriptionService.Notify(createdMessage, MessageActionKind.Create); - createdMessages.Add(createdMessage); - } + await commandExecutor.Execute(new(sentMessageInput.Text, currentUserId, chat.Id)); return createdMessages; }) 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"); From 33c0ff29209ddf6539b106a469269f4735e00382 Mon Sep 17 00:00:00 2001 From: Vitalii Vaskivskyi Date: Mon, 30 Sep 2024 13:30:24 +0300 Subject: [PATCH 3/5] send message by part during generating --- .../Commands/CommandModuleAttribute.cs | 6 ---- .../Commands/Modules/ChatGptModule.cs | 33 ++++++++++++------- 2 files changed, 22 insertions(+), 17 deletions(-) delete mode 100644 Geesemon.Web/Commands/CommandModuleAttribute.cs diff --git a/Geesemon.Web/Commands/CommandModuleAttribute.cs b/Geesemon.Web/Commands/CommandModuleAttribute.cs deleted file mode 100644 index ff8a3a0..0000000 --- a/Geesemon.Web/Commands/CommandModuleAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Geesemon.Web.Commands; - -[AttributeUsage(AttributeTargets.Class)] -public class CommandModuleAttribute : Attribute -{ -} diff --git a/Geesemon.Web/Commands/Modules/ChatGptModule.cs b/Geesemon.Web/Commands/Modules/ChatGptModule.cs index 38d3b85..571a965 100644 --- a/Geesemon.Web/Commands/Modules/ChatGptModule.cs +++ b/Geesemon.Web/Commands/Modules/ChatGptModule.cs @@ -10,7 +10,8 @@ namespace Geesemon.Web.Commands.Modules; public class ChatGptModule( ISettingsProvider settingsProvider, MessageProvider messageProvider, - IMessageActionSubscriptionService messageActionSubscriptionService) : ICommandModule + IMessageActionSubscriptionService messageActionSubscriptionService) + : ICommandModule { readonly MessageProvider messageProvider = messageProvider; readonly IMessageActionSubscriptionService messageActionSubscriptionService = messageActionSubscriptionService; @@ -23,16 +24,26 @@ public async Task Ai(CommandContext context) conversation.AppendUserInput(context.Message); - var chatGptResponse = await conversation.GetResponseFromChatbotAsync(); - - var newMessage = new Message + Message? message = null; + await foreach (var messagePart in conversation.StreamResponseEnumerableFromChatbotAsync()) { - ChatId = context.ChatId, - Text = chatGptResponse, - FromId = context.FromId, - }; - - var createdMessage = await messageProvider.CreateAsync(newMessage); - messageActionSubscriptionService.Notify(createdMessage, MessageActionKind.Create); + 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); + } } } From 1761ccb21f5c243d4bae425c155cb7dd82034e5d Mon Sep 17 00:00:00 2001 From: Vitalii Vaskivskyi Date: Mon, 30 Sep 2024 13:35:47 +0300 Subject: [PATCH 4/5] remove extra context --- Geesemon.Web/Commands/CommandExecutor.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Geesemon.Web/Commands/CommandExecutor.cs b/Geesemon.Web/Commands/CommandExecutor.cs index de74831..e01f9d5 100644 --- a/Geesemon.Web/Commands/CommandExecutor.cs +++ b/Geesemon.Web/Commands/CommandExecutor.cs @@ -24,7 +24,7 @@ public CommandExecutor(IEnumerable commandModules) } } - public async Task Execute(Context context) + public async Task Execute(CommandContext context) { foreach (var commandInfo in commandInfos) { @@ -34,13 +34,12 @@ public async Task Execute(Context context) { var message = context.Message[command.Length..]; - var commandContext = new CommandContext(message, context.FromId, context.ChatId); - await (Task)commandInfo.CommandMethod.Invoke(commandInfo.Module, [commandContext]); + context = context with { Message = message }; + + await (Task)commandInfo.CommandMethod.Invoke(commandInfo.Module, [context]); } } } - - public readonly record struct Context(string Message, Guid FromId, Guid ChatId); } readonly record struct CommandInfo(ICommandModule Module, MethodInfo CommandMethod, string Command); From 15ebc9915d874042f791d9cbda99dff420a1f82f Mon Sep 17 00:00:00 2001 From: Vitalii Vaskivskyi Date: Mon, 30 Sep 2024 15:32:23 +0300 Subject: [PATCH 5/5] add exact match of command in executor --- Geesemon.Web/Commands/CommandExecutor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Geesemon.Web/Commands/CommandExecutor.cs b/Geesemon.Web/Commands/CommandExecutor.cs index e01f9d5..36b8903 100644 --- a/Geesemon.Web/Commands/CommandExecutor.cs +++ b/Geesemon.Web/Commands/CommandExecutor.cs @@ -28,6 +28,12 @@ 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)) @@ -37,6 +43,7 @@ public async Task Execute(CommandContext context) context = context with { Message = message }; await (Task)commandInfo.CommandMethod.Invoke(commandInfo.Module, [context]); + return; } } }