From 2557c2b47eb4f416c2814c24e2d378cdd03bfcdb Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 9 Jan 2026 15:30:52 -0600 Subject: [PATCH 01/36] init message queue --- BotSharp.sln | 11 +++++++ .../BotSharp.Plugin.MessageQueue.csproj | 17 ++++++++++ .../MessageQueuePlugin.cs | 18 +++++++++++ .../Settings/MessageQueueSettings.cs | 5 +++ .../BotSharp.Plugin.MessageQueue/Using.cs | 31 +++++++++++++++++++ src/WebStarter/WebStarter.csproj | 1 + src/WebStarter/appsettings.json | 3 +- 7 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs diff --git a/BotSharp.sln b/BotSharp.sln index ad95f29e8..f68bd7fca 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -157,6 +157,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.A2A", "src\In EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MultiTenancy", "src\Plugins\BotSharp.Plugin.MultiTenancy\BotSharp.Plugin.MultiTenancy.csproj", "{562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MessageQueue", "src\Plugins\BotSharp.Plugin.MessageQueue\BotSharp.Plugin.MessageQueue.csproj", "{C979BAFA-F47D-4709-AB19-E09612E9160E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -669,6 +671,14 @@ Global {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|Any CPU.Build.0 = Release|Any CPU {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|x64.ActiveCfg = Release|Any CPU {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|x64.Build.0 = Release|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|x64.ActiveCfg = Debug|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|x64.Build.0 = Debug|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|Any CPU.Build.0 = Release|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|x64.ActiveCfg = Release|Any CPU + {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -745,6 +755,7 @@ Global {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} {E8D01281-D52A-BFF4-33DB-E35D91754272} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {C979BAFA-F47D-4709-AB19-E09612E9160E} = {51AFE054-AE99-497D-A593-69BAEFB5106F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj b/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj new file mode 100644 index 000000000..4f4c2ce91 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj @@ -0,0 +1,17 @@ + + + + $(TargetFramework) + enable + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(GenerateDocumentationFile) + $(SolutionDir)packages + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs new file mode 100644 index 000000000..bb2982c22 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Plugin.MessageQueue; + +public class MessageQueuePlugin : IBotSharpPlugin +{ + public string Id => "bac8bbf3-da91-4c92-98d8-db14d68e75ae"; + public string Name => "Message queue"; + public string Description => "Handle AI messages in queue."; + public string IconUrl => "https://icon-library.com/images/message-queue-icon/message-queue-icon-13.jpg"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + var settings = new MessageQueueSettings(); + config.Bind("MessageQueue", settings); + services.AddSingleton(settings); + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs new file mode 100644 index 000000000..a7f762bdd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs @@ -0,0 +1,5 @@ +namespace BotSharp.Plugin.MessageQueue.Settings; + +public class MessageQueueSettings +{ +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs new file mode 100644 index 000000000..064e2d643 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs @@ -0,0 +1,31 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Linq; +global using System.Text.Json; +global using System.Net.Mime; +global using System.Threading.Tasks; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Plugins; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Functions; +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Files.Enums; +global using BotSharp.Abstraction.Files.Models; +global using BotSharp.Abstraction.Files.Converters; +global using BotSharp.Abstraction.Files; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Abstraction.Agents.Settings; +global using BotSharp.Abstraction.Functions.Models; +global using BotSharp.Abstraction.Repositories; +global using BotSharp.Abstraction.Settings; +global using BotSharp.Abstraction.Messaging; +global using BotSharp.Abstraction.Messaging.Models.RichContent; +global using BotSharp.Abstraction.Options; + +global using BotSharp.Plugin.MessageQueue.Settings; diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 9374d95fd..7a56f5956 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -39,6 +39,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 39587b64e..4b956e99c 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1061,7 +1061,8 @@ "BotSharp.Plugin.PythonInterpreter", "BotSharp.Plugin.FuzzySharp", "BotSharp.Plugin.MMPEmbedding", - "BotSharp.Plugin.MultiTenancy" + "BotSharp.Plugin.MultiTenancy", + "BotSharp.Plugin.MessageQueue" ] }, From 8f23c64513734e61d7408b0431a445ca97e12f01 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 9 Jan 2026 17:04:22 -0600 Subject: [PATCH 02/36] init mq connection and service --- Directory.Packages.props | 1 + .../BotSharp.Plugin.MessageQueue.csproj | 4 + .../Connections/MQConnection.cs | 137 ++++++++++++++++++ .../Interfaces/IMQConnection.cs | 11 ++ .../Interfaces/IMQService.cs | 7 + .../MessageQueuePlugin.cs | 12 ++ .../Models/MQMessage.cs | 14 ++ .../Services/MQService.cs | 62 ++++++++ .../Settings/MessageQueueSettings.cs | 5 + src/WebStarter/appsettings.json | 10 ++ 10 files changed, 263 insertions(+) create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c198a828..96897fb92 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj b/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj index 4f4c2ce91..2775def6b 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj @@ -10,6 +10,10 @@ $(SolutionDir)packages + + + + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs new file mode 100644 index 000000000..f312ff025 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs @@ -0,0 +1,137 @@ +using BotSharp.Plugin.MessageQueue.Interfaces; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.IO; +using System.Threading; + +namespace BotSharp.Plugin.MessageQueue.Connections; + +public class MQConnection : IMQConnection +{ + private readonly IConnectionFactory _connectionFactory; + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly ILogger _logger; + + private IConnection _connection; + private bool _disposed; + + public MQConnection( + MessageQueueSettings settings, + ILogger logger) + { + _logger = logger; + _connectionFactory = new ConnectionFactory + { + HostName = settings.HostName, + Port = settings.Port, + UserName = settings.UserName, + Password = settings.Password, + VirtualHost = settings.VirtualHost, + ConsumerDispatchConcurrency = 1, + //DispatchConsumersAsync = true, + AutomaticRecoveryEnabled = true, + HandshakeContinuationTimeout = TimeSpan.FromSeconds(20) + }; + } + + public bool IsConnected + { + get + { + return _connection != null && _connection.IsOpen && !_disposed; + } + } + + public IConnection Connection => _connection; + + public async Task CreateChannelAsync() + { + if (!IsConnected) + { + throw new InvalidOperationException("RabbitMQ not connectioned."); + } + return await _connection.CreateChannelAsync(); + } + + public async Task TryConnectAsync() + { + _lock.Wait(); + + if (IsConnected) + { + return true; + } + + _connection = await _connectionFactory.CreateConnectionAsync(); + if (IsConnected) + { + _connection.ConnectionShutdownAsync += OnConnectionShutdownAsync; + _connection.CallbackExceptionAsync += OnCallbackExceptionAsync; + _connection.ConnectionBlockedAsync += OnConnectionBlockedAsync; + _logger.LogInformation($"RabbitMQ client connection success. host: {_connection.Endpoint.HostName} port: {_connection.Endpoint.Port} localPort:{_connection.LocalPort}"); + return true; + } + _logger.LogError("RabbitMQ client connection error."); + return false; + } + + private Task OnConnectionShutdownAsync(object sender, ShutdownEventArgs e) + { + if (_disposed) + { + return Task.CompletedTask; + } + + _logger.LogError($"RabbitMQ connection is on shutdown. Trying to re connect,{e.ReplyCode}:{e.ReplyText}"); + return Task.CompletedTask; + } + + private Task OnCallbackExceptionAsync(object sender, CallbackExceptionEventArgs e) + { + if (_disposed) + { + return Task.CompletedTask; + } + + _logger.LogError($"RabbitMQ connection throw exception. Trying to re connect, {e.Exception}"); + return Task.CompletedTask; + } + + private Task OnConnectionBlockedAsync(object sender, ConnectionBlockedEventArgs e) + { + if (_disposed) + { + return Task.CompletedTask; + } + + _logger.LogError($"RabbitMQ connection is shutdown. Trying to re connect, {e.Reason}"); + return Task.CompletedTask; + } + + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _logger.LogWarning("RabbitMQConnection Dispose()."); + if (_disposed) return; + + _disposed = true; + try + { + _connection.Dispose(); + _logger.LogWarning("RabbitMQConnection Disposed."); + } + catch (IOException ex) + { + _logger.LogError(ex.ToString()); + } + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs new file mode 100644 index 000000000..9acd43474 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs @@ -0,0 +1,11 @@ +using RabbitMQ.Client; + +namespace BotSharp.Plugin.MessageQueue.Interfaces; + +public interface IMQConnection : IDisposable +{ + IConnection Connection { get; } + bool IsConnected { get; } + Task CreateChannelAsync(); + Task TryConnectAsync(); +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs new file mode 100644 index 000000000..70ae9ed85 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Plugin.MessageQueue.Interfaces; + +public interface IMQService +{ + Task PublishAsync(T payload, string exchange, string routingkey, long milliseconds = 0, string messageId = ""); + Task SubscribeAsync(string key, object consumer); +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs index bb2982c22..83ed8f1c4 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs @@ -1,3 +1,7 @@ +using BotSharp.Plugin.MessageQueue.Connections; +using BotSharp.Plugin.MessageQueue.Interfaces; +using BotSharp.Plugin.MessageQueue.Services; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; namespace BotSharp.Plugin.MessageQueue; @@ -14,5 +18,13 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) var settings = new MessageQueueSettings(); config.Bind("MessageQueue", settings); services.AddSingleton(settings); + + services.AddSingleton(); + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app) + { + } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs new file mode 100644 index 000000000..2661447eb --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Plugin.MessageQueue.Models; + +public class MQMessage +{ + public MQMessage(T payload, string messageId) + { + Payload = payload; + MessageId = messageId; + } + + public T Payload { get; set; } + public string MessageId { get; set; } + public DateTime CreateDate { get; set; } = DateTime.UtcNow; +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs new file mode 100644 index 000000000..cf3833be1 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs @@ -0,0 +1,62 @@ +using BotSharp.Plugin.MessageQueue.Interfaces; +using BotSharp.Plugin.MessageQueue.Models; +using RabbitMQ.Client; + +namespace BotSharp.Plugin.MessageQueue.Services; + +public class MQService : IMQService +{ + private IMQConnection _mqConnection; + private readonly ILogger _logger; + + public MQService( + IMQConnection mqConnection, + ILogger logger) + { + _mqConnection = mqConnection; + _logger = logger; + } + + public Task SubscribeAsync(string key, object consumer) + { + throw new NotImplementedException(); + } + + public async Task PublishAsync(T payload, string exchange, string routingkey, long milliseconds = 0, string messageId = "") + { + if (!_mqConnection.IsConnected) + { + await _mqConnection.TryConnectAsync(); + } + + await using var channel = await _mqConnection.CreateChannelAsync(); + var args = new Dictionary + { + {"x-delayed-type", "direct"} + }; + + await channel.ExchangeDeclareAsync(exchange, "x-delayed-message", true, false, args); + + var message = new MQMessage(payload, messageId); + var body = ConvertToBinary(message); + var properties = new BasicProperties + { + MessageId = messageId, + DeliveryMode = DeliveryModes.Persistent, + Headers = new Dictionary + { + { "x-delay", milliseconds } + } + }; + + await channel.BasicPublishAsync(exchange: exchange, routingKey: routingkey, mandatory: true, basicProperties: properties, body: body); + return true; + } + + private byte[] ConvertToBinary(T data) + { + var jsonStr = JsonSerializer.Serialize(data); + var body = Encoding.UTF8.GetBytes(jsonStr); + return body; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs index a7f762bdd..95bc5bf0b 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs @@ -2,4 +2,9 @@ namespace BotSharp.Plugin.MessageQueue.Settings; public class MessageQueueSettings { + public string HostName { get; set; } + public int Port { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public string VirtualHost { get; set; } } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 4b956e99c..84caee211 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1006,6 +1006,7 @@ "Language": "en" } }, + "A2AIntegration": { "Enabled": true, "DefaultTimeoutSeconds": 30, @@ -1018,6 +1019,15 @@ } ] }, + + "MessageQueue": { + "HostName": "localhost", + "Port": 5672, + "UserName": "guest", + "Password": "guest", + "VirtualHost": "/" + }, + "PluginLoader": { "Assemblies": [ "BotSharp.Core", From e7a169db4dcc8afbaab346817ce2117db6ee9841 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 9 Jan 2026 19:00:35 -0600 Subject: [PATCH 03/36] relocate --- BotSharp.sln | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/BotSharp.sln b/BotSharp.sln index f68bd7fca..6cbc657d7 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -157,7 +157,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.A2A", "src\In EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MultiTenancy", "src\Plugins\BotSharp.Plugin.MultiTenancy\BotSharp.Plugin.MultiTenancy.csproj", "{562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MessageQueue", "src\Plugins\BotSharp.Plugin.MessageQueue\BotSharp.Plugin.MessageQueue.csproj", "{C979BAFA-F47D-4709-AB19-E09612E9160E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MessageQueue", "src\Plugins\BotSharp.Plugin.MessageQueue\BotSharp.Plugin.MessageQueue.csproj", "{42848896-0A37-8993-E5AB-47C6475FF1CE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -671,14 +671,14 @@ Global {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|Any CPU.Build.0 = Release|Any CPU {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|x64.ActiveCfg = Release|Any CPU {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|x64.Build.0 = Release|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|x64.ActiveCfg = Debug|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Debug|x64.Build.0 = Debug|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|Any CPU.Build.0 = Release|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|x64.ActiveCfg = Release|Any CPU - {C979BAFA-F47D-4709-AB19-E09612E9160E}.Release|x64.Build.0 = Release|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|x64.ActiveCfg = Debug|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|x64.Build.0 = Debug|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|Any CPU.Build.0 = Release|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|x64.ActiveCfg = Release|Any CPU + {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -755,7 +755,7 @@ Global {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} {E8D01281-D52A-BFF4-33DB-E35D91754272} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76} = {51AFE054-AE99-497D-A593-69BAEFB5106F} - {C979BAFA-F47D-4709-AB19-E09612E9160E} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {42848896-0A37-8993-E5AB-47C6475FF1CE} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} From 2c5ae643d5ab76bb61be1d953cc13785e5902ae8 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 9 Jan 2026 19:05:36 -0600 Subject: [PATCH 04/36] minor change --- .../MessageQueuePlugin.cs | 4 ++-- .../Services/MQService.cs | 20 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs index 83ed8f1c4..4cc6baaa1 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs @@ -9,8 +9,8 @@ namespace BotSharp.Plugin.MessageQueue; public class MessageQueuePlugin : IBotSharpPlugin { public string Id => "bac8bbf3-da91-4c92-98d8-db14d68e75ae"; - public string Name => "Message queue"; - public string Description => "Handle AI messages in queue."; + public string Name => "Message Queue"; + public string Description => "Handle AI messages in RabbitMQ."; public string IconUrl => "https://icon-library.com/images/message-queue-icon/message-queue-icon-13.jpg"; public void RegisterDI(IServiceCollection services, IConfiguration config) diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs index cf3833be1..3729a339f 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs @@ -30,12 +30,17 @@ public async Task PublishAsync(T payload, string exchange, string routi } await using var channel = await _mqConnection.CreateChannelAsync(); - var args = new Dictionary + var args = new Dictionary { - {"x-delayed-type", "direct"} + ["x-delayed-type"] = "direct" }; - await channel.ExchangeDeclareAsync(exchange, "x-delayed-message", true, false, args); + await channel.ExchangeDeclareAsync( + exchange: exchange, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); var message = new MQMessage(payload, messageId); var body = ConvertToBinary(message); @@ -45,11 +50,16 @@ public async Task PublishAsync(T payload, string exchange, string routi DeliveryMode = DeliveryModes.Persistent, Headers = new Dictionary { - { "x-delay", milliseconds } + ["x-delay"] = milliseconds } }; - await channel.BasicPublishAsync(exchange: exchange, routingKey: routingkey, mandatory: true, basicProperties: properties, body: body); + await channel.BasicPublishAsync( + exchange: exchange, + routingKey: routingkey, + mandatory: true, + basicProperties: properties, + body: body); return true; } From 4e46bc5ed6feb15c4e47b7ef15df2cc98d529a2b Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 9 Jan 2026 19:38:40 -0600 Subject: [PATCH 05/36] minor change --- .../Connections/MQConnection.cs | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs index f312ff025..787bdcd65 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs @@ -48,7 +48,7 @@ public async Task CreateChannelAsync() { if (!IsConnected) { - throw new InvalidOperationException("RabbitMQ not connectioned."); + throw new InvalidOperationException("Rabbit MQ is not connectioned."); } return await _connection.CreateChannelAsync(); } @@ -68,10 +68,10 @@ public async Task TryConnectAsync() _connection.ConnectionShutdownAsync += OnConnectionShutdownAsync; _connection.CallbackExceptionAsync += OnCallbackExceptionAsync; _connection.ConnectionBlockedAsync += OnConnectionBlockedAsync; - _logger.LogInformation($"RabbitMQ client connection success. host: {_connection.Endpoint.HostName} port: {_connection.Endpoint.Port} localPort:{_connection.LocalPort}"); + _logger.LogInformation($"Rabbit MQ client connection success. host: {_connection.Endpoint.HostName}, port: {_connection.Endpoint.Port}, localPort:{_connection.LocalPort}"); return true; } - _logger.LogError("RabbitMQ client connection error."); + _logger.LogError("Rabbit MQ client connection error."); return false; } @@ -82,7 +82,7 @@ private Task OnConnectionShutdownAsync(object sender, ShutdownEventArgs e) return Task.CompletedTask; } - _logger.LogError($"RabbitMQ connection is on shutdown. Trying to re connect,{e.ReplyCode}:{e.ReplyText}"); + _logger.LogError($"Rabbit MQ connection is on shutdown. Trying to reconnect, {e.ReplyCode}:{e.ReplyText}."); return Task.CompletedTask; } @@ -93,7 +93,7 @@ private Task OnCallbackExceptionAsync(object sender, CallbackExceptionEventArgs return Task.CompletedTask; } - _logger.LogError($"RabbitMQ connection throw exception. Trying to re connect, {e.Exception}"); + _logger.LogError($"Rabbit MQ connection throw exception. Trying to reconnect, {e.Exception}."); return Task.CompletedTask; } @@ -104,7 +104,7 @@ private Task OnConnectionBlockedAsync(object sender, ConnectionBlockedEventArgs return Task.CompletedTask; } - _logger.LogError($"RabbitMQ connection is shutdown. Trying to re connect, {e.Reason}"); + _logger.LogError($"Rabbit MQ connection is shutdown. Trying to reconnect, {e.Reason}."); return Task.CompletedTask; } @@ -117,21 +117,26 @@ public void Dispose() protected virtual void Dispose(bool disposing) { - if (disposing) + if (!disposing) { - _logger.LogWarning("RabbitMQConnection Dispose()."); - if (_disposed) return; - - _disposed = true; - try - { - _connection.Dispose(); - _logger.LogWarning("RabbitMQConnection Disposed."); - } - catch (IOException ex) - { - _logger.LogError(ex.ToString()); - } + return; + } + + _logger.LogWarning("Disposing Rabbit MQ connection."); + if (_disposed) + { + return; + } + + _disposed = true; + try + { + _connection.Dispose(); + _logger.LogWarning("Disposed Rabbit MQ connection."); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); } } } From d0249eb8a7881edfeed6312e64faa1180dadfafd Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 12 Jan 2026 17:28:52 -0600 Subject: [PATCH 06/36] refine mq --- .../Connections/MQConnection.cs | 65 ++++----- .../Consumers/MQConsumerBase.cs | 138 ++++++++++++++++++ .../Consumers/ScheduledMessageConsumer.cs | 26 ++++ .../Controllers/MessageQueueController.cs | 66 +++++++++ .../Interfaces/IMQConnection.cs | 3 +- .../Interfaces/IMQService.cs | 18 ++- .../MessageQueuePlugin.cs | 9 +- .../Models/ConversationMessagePayload.cs | 76 ++++++++++ .../Models/PublishDelayedMessageRequest.cs | 46 ++++++ .../Models/ScheduledMessagePayload.cs | 31 ++++ .../Services/MQService.cs | 14 +- .../Settings/MessageQueueSettings.cs | 15 +- .../BotSharp.Plugin.MessageQueue/Using.cs | 4 + 13 files changed, 462 insertions(+), 49 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs create mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs index 787bdcd65..3338cc887 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs @@ -9,11 +9,11 @@ namespace BotSharp.Plugin.MessageQueue.Connections; public class MQConnection : IMQConnection { private readonly IConnectionFactory _connectionFactory; - private readonly SemaphoreSlim _lock = new(1, 1); + private readonly SemaphoreSlim _lock = new(initialCount: 1, maxCount: 1); private readonly ILogger _logger; private IConnection _connection; - private bool _disposed; + private bool _disposed = false; public MQConnection( MessageQueueSettings settings, @@ -28,21 +28,12 @@ public MQConnection( Password = settings.Password, VirtualHost = settings.VirtualHost, ConsumerDispatchConcurrency = 1, - //DispatchConsumersAsync = true, AutomaticRecoveryEnabled = true, HandshakeContinuationTimeout = TimeSpan.FromSeconds(20) }; } - public bool IsConnected - { - get - { - return _connection != null && _connection.IsOpen && !_disposed; - } - } - - public IConnection Connection => _connection; + public bool IsConnected => _connection != null && _connection.IsOpen && !_disposed; public async Task CreateChannelAsync() { @@ -53,26 +44,34 @@ public async Task CreateChannelAsync() return await _connection.CreateChannelAsync(); } - public async Task TryConnectAsync() + public async Task ConnectAsync() { - _lock.Wait(); + await _lock.WaitAsync(); - if (IsConnected) + try { - return true; + if (IsConnected) + { + return true; + } + + _connection = await _connectionFactory.CreateConnectionAsync(); + if (IsConnected) + { + _connection.ConnectionShutdownAsync += OnConnectionShutdownAsync; + _connection.CallbackExceptionAsync += OnCallbackExceptionAsync; + _connection.ConnectionBlockedAsync += OnConnectionBlockedAsync; + _logger.LogInformation($"Rabbit MQ client connection success. host: {_connection.Endpoint.HostName}, port: {_connection.Endpoint.Port}, localPort:{_connection.LocalPort}"); + return true; + } + _logger.LogError("Rabbit MQ client connection error."); + return false; } - - _connection = await _connectionFactory.CreateConnectionAsync(); - if (IsConnected) + finally { - _connection.ConnectionShutdownAsync += OnConnectionShutdownAsync; - _connection.CallbackExceptionAsync += OnCallbackExceptionAsync; - _connection.ConnectionBlockedAsync += OnConnectionBlockedAsync; - _logger.LogInformation($"Rabbit MQ client connection success. host: {_connection.Endpoint.HostName}, port: {_connection.Endpoint.Port}, localPort:{_connection.LocalPort}"); - return true; + _lock.Release(); } - _logger.LogError("Rabbit MQ client connection error."); - return false; + } private Task OnConnectionShutdownAsync(object sender, ShutdownEventArgs e) @@ -82,7 +81,7 @@ private Task OnConnectionShutdownAsync(object sender, ShutdownEventArgs e) return Task.CompletedTask; } - _logger.LogError($"Rabbit MQ connection is on shutdown. Trying to reconnect, {e.ReplyCode}:{e.ReplyText}."); + _logger.LogError($"Rabbit MQ connection is shutdown. {e}."); return Task.CompletedTask; } @@ -117,26 +116,22 @@ public void Dispose() protected virtual void Dispose(bool disposing) { - if (!disposing) + if (!disposing || _disposed) { return; } - _logger.LogWarning("Disposing Rabbit MQ connection."); - if (_disposed) - { - return; - } + _logger.LogWarning("Start disposing Rabbit MQ connection."); - _disposed = true; try { _connection.Dispose(); + _disposed = true; _logger.LogWarning("Disposed Rabbit MQ connection."); } catch (Exception ex) { - _logger.LogError(ex, ex.Message); + _logger.LogError(ex, $"Error when disposing Rabbit MQ connection"); } } } diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs new file mode 100644 index 000000000..4ca15cccd --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs @@ -0,0 +1,138 @@ +using BotSharp.Plugin.MessageQueue.Interfaces; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace BotSharp.Plugin.MessageQueue.Consumers; + +public abstract class MQConsumerBase : IDisposable +{ + protected readonly IServiceProvider _services; + protected readonly IMQConnection _mqConnection; + protected readonly ILogger _logger; + + private IChannel? _channel; + private bool _disposed = false; + + protected abstract string ExchangeName { get; } + protected abstract string QueueName { get; } + protected abstract string RoutingKey { get; } + + protected MQConsumerBase( + IServiceProvider services, + IMQConnection mqConnection, + ILogger logger) + { + _services = services; + _mqConnection = mqConnection; + _logger = logger; + InitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + protected abstract Task OnMessageReceiveHandle(string data); + + private async Task InitAsync() + { + _channel = await CreateChannelAsync(); + await InitConsumeAsync(); + } + + private async Task CreateChannelAsync() + { + if (!_mqConnection.IsConnected) + { + await _mqConnection.ConnectAsync(); + } + + var channel = await _mqConnection.CreateChannelAsync(); + _logger.LogWarning($"Created Rabbit MQ channel {channel.ChannelNumber}"); + + var args = new Dictionary + { + ["x-delayed-type"] = "direct" + }; + + await channel.ExchangeDeclareAsync( + exchange: ExchangeName, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); + + await channel.QueueDeclareAsync( + queue: QueueName, + durable: true, + exclusive: false, + autoDelete: false); + + await channel.QueueBindAsync(queue: QueueName, exchange: ExchangeName, routingKey: RoutingKey); + channel.ChannelShutdownAsync += async (sender, evt) => + { + if (_disposed || !_mqConnection.IsConnected) + { + return; + } + + _channel?.Dispose(); + await InitAsync(); + }; + + return channel; + } + + private async Task InitConsumeAsync() + { + _logger.LogWarning($"Rabbit MQ starts consuming ({QueueName}) message."); + + if (_channel == null) + { + throw new Exception($"Undefined channel for queue {QueueName}."); + } + + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += ConsumeEventAsync; + await _channel.BasicConsumeAsync(queue: QueueName, autoAck: false, consumer: consumer); + + _logger.LogWarning($"Rabbit MQ consumed ({QueueName}) message."); + } + + private async Task ConsumeEventAsync(object sender, BasicDeliverEventArgs eventArgs) + { + var data = string.Empty; + try + { + data = Encoding.UTF8.GetString(eventArgs.Body.Span); + _logger.LogInformation($"{GetType().Name} message id:{eventArgs.BasicProperties?.MessageId}, data: {data}"); + await OnMessageReceiveHandle(data); + + await _channel!.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when Rabbit MQ consumes data ({data}) in {QueueName}."); + await _channel!.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing || _disposed) + { + return; + } + + _logger.LogWarning($"Start disposing consumer channel: {QueueName}"); + if (_channel != null) + { + _channel.Dispose(); + _disposed = true; + _logger.LogWarning($"Disposed consumer channel: {QueueName}"); + } + } +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs new file mode 100644 index 000000000..dfc1856b1 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs @@ -0,0 +1,26 @@ +using BotSharp.Plugin.MessageQueue.Interfaces; + +namespace BotSharp.Plugin.MessageQueue.Consumers; + + +public class ScheduledMessageConsumer : MQConsumerBase +{ + protected override string ExchangeName => "scheduled.exchange"; + protected override string QueueName => "scheduled.queue"; + protected override string RoutingKey => "scheduled.routing"; + + public ScheduledMessageConsumer( + IServiceProvider services, + IMQConnection mqConnection, + ILogger logger) + : base(services, mqConnection, logger) + { + } + + protected override async Task OnMessageReceiveHandle(string data) + { + _logger.LogCritical($"Received delayed message data: {data}"); + return true; + } +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs new file mode 100644 index 000000000..571a0430c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs @@ -0,0 +1,66 @@ +using BotSharp.Plugin.MessageQueue.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace BotSharp.Plugin.MessageQueue.Controllers; + +/// +/// Controller for publishing delayed messages to the message queue +/// +[Authorize] +[ApiController] +public class MessageQueueController : ControllerBase +{ + private readonly IServiceProvider _services; + private readonly IMQService _mqService; + private readonly ILogger _logger; + + public MessageQueueController( + IServiceProvider services, + IMQService mqService, + ILogger logger) + { + _services = services; + _mqService = mqService; + _logger = logger; + } + + /// + /// Publish a scheduled message to be delivered after a delay + /// + /// The scheduled message request + /// Publish result with message ID and expected delivery time + [HttpPost("/message-queue/scheduled")] + public async Task PublishScheduledMessage([FromBody] PublishScheduledMessageRequest request) + { + if (request == null) + { + return BadRequest(new PublishMessageResponse { Success = false, Error = "Request body is required." }); + } + + try + { + var payload = new ScheduledMessagePayload + { + Name = request.Name ?? "Hello" + }; + + var success = await _mqService.PublishAsync( + payload, + exchange: "scheduled.exchange", + routingkey: "scheduled.routing", + milliseconds: request.DelayMilliseconds ?? 10000, + messageId: request.MessageId ?? Guid.NewGuid().ToString()); + + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to publish scheduled message"); + return StatusCode(StatusCodes.Status500InternalServerError, + new PublishMessageResponse { Success = false, Error = ex.Message }); + } + } +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs index 9acd43474..2f65c26c3 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs @@ -4,8 +4,7 @@ namespace BotSharp.Plugin.MessageQueue.Interfaces; public interface IMQConnection : IDisposable { - IConnection Connection { get; } bool IsConnected { get; } Task CreateChannelAsync(); - Task TryConnectAsync(); + Task ConnectAsync(); } diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs index 70ae9ed85..a17ff176a 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs @@ -2,6 +2,22 @@ namespace BotSharp.Plugin.MessageQueue.Interfaces; public interface IMQService { + /// + /// Subscribe consumer + /// + /// + /// + void Subscribe(string key, object consumer); + + /// + /// Publish payload to message queue + /// + /// + /// + /// + /// + /// + /// + /// Task PublishAsync(T payload, string exchange, string routingkey, long milliseconds = 0, string messageId = ""); - Task SubscribeAsync(string key, object consumer); } diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs index 4cc6baaa1..d95d265d7 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs @@ -6,9 +6,9 @@ namespace BotSharp.Plugin.MessageQueue; -public class MessageQueuePlugin : IBotSharpPlugin +public class MessageQueuePlugin : IBotSharpAppPlugin { - public string Id => "bac8bbf3-da91-4c92-98d8-db14d68e75ae"; + public string Id => "3f93407f-3c37-4e25-be28-142a2da9b514"; public string Name => "Message Queue"; public string Description => "Handle AI messages in RabbitMQ."; public string IconUrl => "https://icon-library.com/images/message-queue-icon/message-queue-icon-13.jpg"; @@ -25,6 +25,11 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) public void Configure(IApplicationBuilder app) { + var sp = app.ApplicationServices; + var mqConnection = sp.GetRequiredService(); + var mqService = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + mqService.Subscribe(nameof(ScheduledMessageConsumer), new ScheduledMessageConsumer(sp, mqConnection, logger)); } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs new file mode 100644 index 000000000..0d977dca5 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs @@ -0,0 +1,76 @@ +using BotSharp.Abstraction.Models; + +namespace BotSharp.Plugin.MessageQueue.Models; + +/// +/// Payload for delayed conversation messages +/// +public class ConversationMessagePayload +{ + /// + /// The action to perform + /// + public ConversationAction Action { get; set; } + + /// + /// The conversation ID + /// + public string? ConversationId { get; set; } + + /// + /// The agent ID to handle the message + /// + public string? AgentId { get; set; } + + /// + /// The user ID associated with this message + /// + public string? UserId { get; set; } + + /// + /// The role of the message sender (User, Assistant, Function, etc.) + /// + public string? Role { get; set; } + + /// + /// The message content + /// + public string? Content { get; set; } + + /// + /// Optional instruction for triggering an agent + /// + public string? Instruction { get; set; } + + /// + /// Conversation states to set + /// + public List? States { get; set; } + + /// + /// Additional metadata + /// + public Dictionary? Metadata { get; set; } +} + +/// +/// Actions that can be performed on a conversation +/// +public enum ConversationAction +{ + /// + /// Send a message to the conversation + /// + SendMessage, + + /// + /// Trigger an agent to respond + /// + TriggerAgent, + + /// + /// Send a notification to the conversation + /// + Notify +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs new file mode 100644 index 000000000..6d51cc10f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs @@ -0,0 +1,46 @@ +namespace BotSharp.Plugin.MessageQueue.Models; + +/// +/// Request model for publishing a scheduled message +/// +public class PublishScheduledMessageRequest +{ + public string? Name { get; set; } + + public long? DelayMilliseconds { get; set; } + + public string? MessageId { get; set; } +} + + +/// +/// Response model for publish operations +/// +public class PublishMessageResponse +{ + /// + /// Whether the message was successfully published + /// + public bool Success { get; set; } + + /// + /// The message ID + /// + public string? MessageId { get; set; } + + /// + /// The calculated delay in milliseconds + /// + public long DelayMilliseconds { get; set; } + + /// + /// The expected delivery time (UTC) + /// + public DateTime ExpectedDeliveryTime { get; set; } + + /// + /// Error message if publish failed + /// + public string? Error { get; set; } +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs new file mode 100644 index 000000000..3967791d2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs @@ -0,0 +1,31 @@ +namespace BotSharp.Plugin.MessageQueue.Models; + +/// +/// Payload for scheduled/delayed messages +/// +public class ScheduledMessagePayload +{ + public string Name { get; set; } +} + +/// +/// Types of scheduled messages +/// +public enum ScheduledMessageType +{ + /// + /// A reminder message to send to a conversation + /// + Reminder, + + /// + /// A follow-up message for a previous conversation + /// + FollowUp, + + /// + /// A scheduled task to execute + /// + Task +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs index 3729a339f..537a652b8 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs @@ -1,6 +1,6 @@ using BotSharp.Plugin.MessageQueue.Interfaces; -using BotSharp.Plugin.MessageQueue.Models; using RabbitMQ.Client; +using System.Collections.Concurrent; namespace BotSharp.Plugin.MessageQueue.Services; @@ -9,6 +9,8 @@ public class MQService : IMQService private IMQConnection _mqConnection; private readonly ILogger _logger; + private static readonly ConcurrentDictionary _consumers = []; + public MQService( IMQConnection mqConnection, ILogger logger) @@ -17,16 +19,20 @@ public MQService( _logger = logger; } - public Task SubscribeAsync(string key, object consumer) + public void Subscribe(string key, object consumer) { - throw new NotImplementedException(); + var baseConsumer = consumer as MQConsumerBase; + if (baseConsumer != null) + { + _consumers.TryAdd(key, baseConsumer); + } } public async Task PublishAsync(T payload, string exchange, string routingkey, long milliseconds = 0, string messageId = "") { if (!_mqConnection.IsConnected) { - await _mqConnection.TryConnectAsync(); + await _mqConnection.ConnectAsync(); } await using var channel = await _mqConnection.CreateChannelAsync(); diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs index 95bc5bf0b..6d3ba0707 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs @@ -2,9 +2,14 @@ namespace BotSharp.Plugin.MessageQueue.Settings; public class MessageQueueSettings { - public string HostName { get; set; } - public int Port { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public string VirtualHost { get; set; } + public string HostName { get; set; } = "localhost"; + public int Port { get; set; } = 5672; + public string UserName { get; set; } = "guest"; + public string Password { get; set; } = "guest"; + public string VirtualHost { get; set; } = "/"; + + /// + /// Enable the message queue consumers for delayed message handling + /// + public bool EnableConsumers { get; set; } = false; } diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs index 064e2d643..637062e8b 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs @@ -27,5 +27,9 @@ global using BotSharp.Abstraction.Messaging; global using BotSharp.Abstraction.Messaging.Models.RichContent; global using BotSharp.Abstraction.Options; +global using BotSharp.Abstraction.Models; global using BotSharp.Plugin.MessageQueue.Settings; +global using BotSharp.Plugin.MessageQueue.Consumers; +global using BotSharp.Plugin.MessageQueue.Models; +global using BotSharp.Plugin.MessageQueue.Controllers; From a7024e0179cc2768855ebde2e064d396021862f8 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 12 Jan 2026 17:33:04 -0600 Subject: [PATCH 07/36] minor change --- .../Connections/MQConnection.cs | 10 +++---- .../Consumers/MQConsumerBase.cs | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs index 3338cc887..951ccb129 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs @@ -110,13 +110,7 @@ private Task OnConnectionBlockedAsync(object sender, ConnectionBlockedEventArgs public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposing || _disposed) + if (_disposed) { return; } @@ -133,5 +127,7 @@ protected virtual void Dispose(bool disposing) { _logger.LogError(ex, $"Error when disposing Rabbit MQ connection"); } + + GC.SuppressFinalize(this); } } diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs index 4ca15cccd..c48bcdd1e 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs +++ b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs @@ -115,24 +115,28 @@ private async Task ConsumeEventAsync(object sender, BasicDeliverEventArgs eventA public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposing || _disposed) + if (! _disposed) { return; } - _logger.LogWarning($"Start disposing consumer channel: {QueueName}"); + var consumerName = GetType().Name; + _logger.LogWarning($"Start disposing consumer: {consumerName}"); if (_channel != null) { - _channel.Dispose(); - _disposed = true; - _logger.LogWarning($"Disposed consumer channel: {QueueName}"); + try + { + _channel.Dispose(); + _disposed = true; + _logger.LogWarning($"Disposed consumer: {consumerName}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when disposing consumer: {consumerName}"); + } } + + GC.SuppressFinalize(this); } } From 56dae9295e37f09976586cb275aa437182535d71 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 20 Jan 2026 00:05:26 -0600 Subject: [PATCH 08/36] refine message queue --- BotSharp.sln | 20 +- Directory.Packages.props | 2 +- .../MessageQueues/IMQConsumer.cs | 24 ++ .../MessageQueues/IMQService.cs | 31 +++ .../MessageQueues/MessageQueueSettings.cs | 7 + .../MessageQueues/Models/MQConsumerOptions.cs | 34 +++ .../MessageQueues}/Models/MQMessage.cs | 2 +- .../MessageQueues/Models/MQPublishOptions.cs | 9 + .../Messaging/MessagingPlugin.cs | 18 ++ .../Consumers/MQConsumerBase.cs | 142 ----------- .../Consumers/ScheduledMessageConsumer.cs | 26 -- .../Interfaces/IMQService.cs | 23 -- .../MessageQueuePlugin.cs | 35 --- .../Models/ConversationMessagePayload.cs | 76 ------ .../Models/ScheduledMessagePayload.cs | 31 --- .../Services/MQService.cs | 78 ------ .../BotSharp.Plugin.RabbitMQ.csproj} | 1 + .../Connections/RabbitMQConnection.cs} | 37 ++- .../Consumers/MQConsumerBase.cs | 51 ++++ .../Consumers/ScheduledMessageConsumer.cs | 27 ++ .../Controllers/RabbitMQController.cs} | 23 +- .../Interfaces/IRabbitMQConnection.cs} | 4 +- .../Models/PublishDelayedMessageRequest.cs | 2 +- .../Models/ScheduledMessagePayload.cs | 9 + .../RabbitMQPlugin.cs | 51 ++++ .../Services/RabbitMQService.cs | 235 ++++++++++++++++++ .../Settings/RabbitMQSettings.cs} | 9 +- .../Using.cs | 10 +- src/WebStarter/WebStarter.csproj | 2 +- src/WebStarter/appsettings.json | 10 +- 30 files changed, 570 insertions(+), 459 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MessageQueueSettings.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs rename src/{Plugins/BotSharp.Plugin.MessageQueue => Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues}/Models/MQMessage.cs (80%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs create mode 100644 src/Infrastructure/BotSharp.Core/Messaging/MessagingPlugin.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs delete mode 100644 src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs rename src/Plugins/{BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj => BotSharp.Plugin.RabbitMQ/BotSharp.Plugin.RabbitMQ.csproj} (94%) rename src/Plugins/{BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs => BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs} (77%) create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs rename src/Plugins/{BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs => BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs} (75%) rename src/Plugins/{BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs => BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs} (57%) rename src/Plugins/{BotSharp.Plugin.MessageQueue => BotSharp.Plugin.RabbitMQ}/Models/PublishDelayedMessageRequest.cs (95%) create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Models/ScheduledMessagePayload.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs rename src/Plugins/{BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs => BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs} (51%) rename src/Plugins/{BotSharp.Plugin.MessageQueue => BotSharp.Plugin.RabbitMQ}/Using.cs (79%) diff --git a/BotSharp.sln b/BotSharp.sln index 6cbc657d7..6abc5b47b 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -157,7 +157,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.A2A", "src\In EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MultiTenancy", "src\Plugins\BotSharp.Plugin.MultiTenancy\BotSharp.Plugin.MultiTenancy.csproj", "{562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.MessageQueue", "src\Plugins\BotSharp.Plugin.MessageQueue\BotSharp.Plugin.MessageQueue.csproj", "{42848896-0A37-8993-E5AB-47C6475FF1CE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.RabbitMQ", "src\Plugins\BotSharp.Plugin.RabbitMQ\BotSharp.Plugin.RabbitMQ.csproj", "{8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -671,14 +671,14 @@ Global {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|Any CPU.Build.0 = Release|Any CPU {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|x64.ActiveCfg = Release|Any CPU {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76}.Release|x64.Build.0 = Release|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|x64.ActiveCfg = Debug|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Debug|x64.Build.0 = Debug|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|Any CPU.Build.0 = Release|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|x64.ActiveCfg = Release|Any CPU - {42848896-0A37-8993-E5AB-47C6475FF1CE}.Release|x64.Build.0 = Release|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Debug|x64.Build.0 = Debug|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Release|Any CPU.Build.0 = Release|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Release|x64.ActiveCfg = Release|Any CPU + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -755,7 +755,7 @@ Global {13223C71-9EAC-9835-28ED-5A4833E6F915} = {53E7CD86-0D19-40D9-A0FA-AB4613837E89} {E8D01281-D52A-BFF4-33DB-E35D91754272} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {562DD0C6-DAC8-02CC-C1DD-D43DF186CE76} = {51AFE054-AE99-497D-A593-69BAEFB5106F} - {42848896-0A37-8993-E5AB-47C6475FF1CE} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00} + {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Packages.props b/Directory.Packages.props index 96897fb92..8e19e4dcb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs new file mode 100644 index 000000000..f0aff8c1b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -0,0 +1,24 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; + +namespace BotSharp.Abstraction.Infrastructures.MessageQueues; + +/// +/// Abstract interface for message queue consumers. +/// Implement this interface to create consumers that are independent of MQ products (e.g., RabbitMQ, Kafka, Azure Service Bus). +/// +public interface IMQConsumer : IDisposable +{ + /// + /// Gets the consumer options containing exchange, queue and routing configuration. + /// + MQConsumerOptions Options { get; } + + /// + /// Handles the received message from the queue. + /// + /// The consumer channel identifier + /// The message data as string + /// True if the message was handled successfully, false otherwise + Task HandleMessageAsync(string channel, string data); +} + diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs new file mode 100644 index 000000000..e77878582 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs @@ -0,0 +1,31 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; + +namespace BotSharp.Abstraction.Infrastructures.MessageQueues; + +public interface IMQService +{ + /// + /// Subscribe a consumer to the message queue. + /// The consumer will be initialized with the appropriate MQ-specific infrastructure. + /// + /// Unique identifier for the consumer + /// The consumer implementing IMQConsumer interface + /// Task representing the async subscription operation + Task SubscribeAsync(string key, IMQConsumer consumer); + + /// + /// Unsubscribe a consumer from the message queue. + /// + /// Unique identifier for the consumer + /// Task representing the async unsubscription operation + Task UnsubscribeAsync(string key); + + /// + /// Publish payload to message queue + /// + /// + /// + /// + /// + Task PublishAsync(T payload, MQPublishOptions options); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MessageQueueSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MessageQueueSettings.cs new file mode 100644 index 000000000..b08a5a054 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MessageQueueSettings.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Infrastructures.MessageQueues; + +public class MessageQueueSettings +{ + public bool Enabled { get; set; } + public string Provider { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs new file mode 100644 index 000000000..7aa9bf02e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs @@ -0,0 +1,34 @@ +namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; + +/// +/// Configuration options for message queue consumers. +/// These options are MQ-product agnostic and can be adapted by different implementations. +/// +public class MQConsumerOptions +{ + /// + /// The exchange name (topic in some MQ systems). + /// + public string ExchangeName { get; set; } = string.Empty; + + /// + /// The queue name (subscription in some MQ systems). + /// + public string QueueName { get; set; } = string.Empty; + + /// + /// The routing key (filter in some MQ systems). + /// + public string RoutingKey { get; set; } = string.Empty; + + /// + /// Whether to automatically acknowledge messages. + /// + public bool AutoAck { get; set; } = false; + + /// + /// Additional arguments for the consumer configuration. + /// + public Dictionary Arguments { get; set; } = new(); +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQMessage.cs similarity index 80% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs rename to src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQMessage.cs index 2661447eb..e940aff01 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/MQMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQMessage.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.MessageQueue.Models; +namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; public class MQMessage { diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs new file mode 100644 index 000000000..e0eba68be --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; + +public class MQPublishOptions +{ + public string Exchange { get; set; } + public string RoutingKey { get; set; } + public long MilliSeconds { get; set; } + public string? MessageId { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core/Messaging/MessagingPlugin.cs b/src/Infrastructure/BotSharp.Core/Messaging/MessagingPlugin.cs new file mode 100644 index 000000000..5c84fcb63 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Messaging/MessagingPlugin.cs @@ -0,0 +1,18 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues; +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Core.Messaging; + +public class MessagingPlugin : IBotSharpPlugin +{ + public string Id => "52a0aa30-4820-42a9-9cae-df0be81bad2b"; + public string Name => "Messaging"; + public string Description => "Provides message queue services."; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + var mqSettings = new MessageQueueSettings(); + config.Bind("MessageQueue", mqSettings); + services.AddSingleton(mqSettings); + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs deleted file mode 100644 index c48bcdd1e..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/MQConsumerBase.cs +++ /dev/null @@ -1,142 +0,0 @@ -using BotSharp.Plugin.MessageQueue.Interfaces; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace BotSharp.Plugin.MessageQueue.Consumers; - -public abstract class MQConsumerBase : IDisposable -{ - protected readonly IServiceProvider _services; - protected readonly IMQConnection _mqConnection; - protected readonly ILogger _logger; - - private IChannel? _channel; - private bool _disposed = false; - - protected abstract string ExchangeName { get; } - protected abstract string QueueName { get; } - protected abstract string RoutingKey { get; } - - protected MQConsumerBase( - IServiceProvider services, - IMQConnection mqConnection, - ILogger logger) - { - _services = services; - _mqConnection = mqConnection; - _logger = logger; - InitAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - } - - protected abstract Task OnMessageReceiveHandle(string data); - - private async Task InitAsync() - { - _channel = await CreateChannelAsync(); - await InitConsumeAsync(); - } - - private async Task CreateChannelAsync() - { - if (!_mqConnection.IsConnected) - { - await _mqConnection.ConnectAsync(); - } - - var channel = await _mqConnection.CreateChannelAsync(); - _logger.LogWarning($"Created Rabbit MQ channel {channel.ChannelNumber}"); - - var args = new Dictionary - { - ["x-delayed-type"] = "direct" - }; - - await channel.ExchangeDeclareAsync( - exchange: ExchangeName, - type: "x-delayed-message", - durable: true, - autoDelete: false, - arguments: args); - - await channel.QueueDeclareAsync( - queue: QueueName, - durable: true, - exclusive: false, - autoDelete: false); - - await channel.QueueBindAsync(queue: QueueName, exchange: ExchangeName, routingKey: RoutingKey); - channel.ChannelShutdownAsync += async (sender, evt) => - { - if (_disposed || !_mqConnection.IsConnected) - { - return; - } - - _channel?.Dispose(); - await InitAsync(); - }; - - return channel; - } - - private async Task InitConsumeAsync() - { - _logger.LogWarning($"Rabbit MQ starts consuming ({QueueName}) message."); - - if (_channel == null) - { - throw new Exception($"Undefined channel for queue {QueueName}."); - } - - var consumer = new AsyncEventingBasicConsumer(_channel); - consumer.ReceivedAsync += ConsumeEventAsync; - await _channel.BasicConsumeAsync(queue: QueueName, autoAck: false, consumer: consumer); - - _logger.LogWarning($"Rabbit MQ consumed ({QueueName}) message."); - } - - private async Task ConsumeEventAsync(object sender, BasicDeliverEventArgs eventArgs) - { - var data = string.Empty; - try - { - data = Encoding.UTF8.GetString(eventArgs.Body.Span); - _logger.LogInformation($"{GetType().Name} message id:{eventArgs.BasicProperties?.MessageId}, data: {data}"); - await OnMessageReceiveHandle(data); - - await _channel!.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when Rabbit MQ consumes data ({data}) in {QueueName}."); - await _channel!.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); - } - } - - public void Dispose() - { - if (! _disposed) - { - return; - } - - var consumerName = GetType().Name; - _logger.LogWarning($"Start disposing consumer: {consumerName}"); - if (_channel != null) - { - try - { - _channel.Dispose(); - _disposed = true; - _logger.LogWarning($"Disposed consumer: {consumerName}"); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when disposing consumer: {consumerName}"); - } - } - - GC.SuppressFinalize(this); - } -} - diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs deleted file mode 100644 index dfc1856b1..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Consumers/ScheduledMessageConsumer.cs +++ /dev/null @@ -1,26 +0,0 @@ -using BotSharp.Plugin.MessageQueue.Interfaces; - -namespace BotSharp.Plugin.MessageQueue.Consumers; - - -public class ScheduledMessageConsumer : MQConsumerBase -{ - protected override string ExchangeName => "scheduled.exchange"; - protected override string QueueName => "scheduled.queue"; - protected override string RoutingKey => "scheduled.routing"; - - public ScheduledMessageConsumer( - IServiceProvider services, - IMQConnection mqConnection, - ILogger logger) - : base(services, mqConnection, logger) - { - } - - protected override async Task OnMessageReceiveHandle(string data) - { - _logger.LogCritical($"Received delayed message data: {data}"); - return true; - } -} - diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs deleted file mode 100644 index a17ff176a..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQService.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace BotSharp.Plugin.MessageQueue.Interfaces; - -public interface IMQService -{ - /// - /// Subscribe consumer - /// - /// - /// - void Subscribe(string key, object consumer); - - /// - /// Publish payload to message queue - /// - /// - /// - /// - /// - /// - /// - /// - Task PublishAsync(T payload, string exchange, string routingkey, long milliseconds = 0, string messageId = ""); -} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs deleted file mode 100644 index d95d265d7..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/MessageQueuePlugin.cs +++ /dev/null @@ -1,35 +0,0 @@ -using BotSharp.Plugin.MessageQueue.Connections; -using BotSharp.Plugin.MessageQueue.Interfaces; -using BotSharp.Plugin.MessageQueue.Services; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; - -namespace BotSharp.Plugin.MessageQueue; - -public class MessageQueuePlugin : IBotSharpAppPlugin -{ - public string Id => "3f93407f-3c37-4e25-be28-142a2da9b514"; - public string Name => "Message Queue"; - public string Description => "Handle AI messages in RabbitMQ."; - public string IconUrl => "https://icon-library.com/images/message-queue-icon/message-queue-icon-13.jpg"; - - public void RegisterDI(IServiceCollection services, IConfiguration config) - { - var settings = new MessageQueueSettings(); - config.Bind("MessageQueue", settings); - services.AddSingleton(settings); - - services.AddSingleton(); - services.AddSingleton(); - } - - public void Configure(IApplicationBuilder app) - { - var sp = app.ApplicationServices; - var mqConnection = sp.GetRequiredService(); - var mqService = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - - mqService.Subscribe(nameof(ScheduledMessageConsumer), new ScheduledMessageConsumer(sp, mqConnection, logger)); - } -} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs deleted file mode 100644 index 0d977dca5..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ConversationMessagePayload.cs +++ /dev/null @@ -1,76 +0,0 @@ -using BotSharp.Abstraction.Models; - -namespace BotSharp.Plugin.MessageQueue.Models; - -/// -/// Payload for delayed conversation messages -/// -public class ConversationMessagePayload -{ - /// - /// The action to perform - /// - public ConversationAction Action { get; set; } - - /// - /// The conversation ID - /// - public string? ConversationId { get; set; } - - /// - /// The agent ID to handle the message - /// - public string? AgentId { get; set; } - - /// - /// The user ID associated with this message - /// - public string? UserId { get; set; } - - /// - /// The role of the message sender (User, Assistant, Function, etc.) - /// - public string? Role { get; set; } - - /// - /// The message content - /// - public string? Content { get; set; } - - /// - /// Optional instruction for triggering an agent - /// - public string? Instruction { get; set; } - - /// - /// Conversation states to set - /// - public List? States { get; set; } - - /// - /// Additional metadata - /// - public Dictionary? Metadata { get; set; } -} - -/// -/// Actions that can be performed on a conversation -/// -public enum ConversationAction -{ - /// - /// Send a message to the conversation - /// - SendMessage, - - /// - /// Trigger an agent to respond - /// - TriggerAgent, - - /// - /// Send a notification to the conversation - /// - Notify -} - diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs deleted file mode 100644 index 3967791d2..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/ScheduledMessagePayload.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace BotSharp.Plugin.MessageQueue.Models; - -/// -/// Payload for scheduled/delayed messages -/// -public class ScheduledMessagePayload -{ - public string Name { get; set; } -} - -/// -/// Types of scheduled messages -/// -public enum ScheduledMessageType -{ - /// - /// A reminder message to send to a conversation - /// - Reminder, - - /// - /// A follow-up message for a previous conversation - /// - FollowUp, - - /// - /// A scheduled task to execute - /// - Task -} - diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs b/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs deleted file mode 100644 index 537a652b8..000000000 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Services/MQService.cs +++ /dev/null @@ -1,78 +0,0 @@ -using BotSharp.Plugin.MessageQueue.Interfaces; -using RabbitMQ.Client; -using System.Collections.Concurrent; - -namespace BotSharp.Plugin.MessageQueue.Services; - -public class MQService : IMQService -{ - private IMQConnection _mqConnection; - private readonly ILogger _logger; - - private static readonly ConcurrentDictionary _consumers = []; - - public MQService( - IMQConnection mqConnection, - ILogger logger) - { - _mqConnection = mqConnection; - _logger = logger; - } - - public void Subscribe(string key, object consumer) - { - var baseConsumer = consumer as MQConsumerBase; - if (baseConsumer != null) - { - _consumers.TryAdd(key, baseConsumer); - } - } - - public async Task PublishAsync(T payload, string exchange, string routingkey, long milliseconds = 0, string messageId = "") - { - if (!_mqConnection.IsConnected) - { - await _mqConnection.ConnectAsync(); - } - - await using var channel = await _mqConnection.CreateChannelAsync(); - var args = new Dictionary - { - ["x-delayed-type"] = "direct" - }; - - await channel.ExchangeDeclareAsync( - exchange: exchange, - type: "x-delayed-message", - durable: true, - autoDelete: false, - arguments: args); - - var message = new MQMessage(payload, messageId); - var body = ConvertToBinary(message); - var properties = new BasicProperties - { - MessageId = messageId, - DeliveryMode = DeliveryModes.Persistent, - Headers = new Dictionary - { - ["x-delay"] = milliseconds - } - }; - - await channel.BasicPublishAsync( - exchange: exchange, - routingKey: routingkey, - mandatory: true, - basicProperties: properties, - body: body); - return true; - } - - private byte[] ConvertToBinary(T data) - { - var jsonStr = JsonSerializer.Serialize(data); - var body = Encoding.UTF8.GetBytes(jsonStr); - return body; - } -} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj b/src/Plugins/BotSharp.Plugin.RabbitMQ/BotSharp.Plugin.RabbitMQ.csproj similarity index 94% rename from src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj rename to src/Plugins/BotSharp.Plugin.RabbitMQ/BotSharp.Plugin.RabbitMQ.csproj index 2775def6b..4a8f3ff20 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/BotSharp.Plugin.MessageQueue.csproj +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/BotSharp.Plugin.RabbitMQ.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs similarity index 77% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs rename to src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs index 951ccb129..ab7055cfd 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Connections/MQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs @@ -1,24 +1,27 @@ -using BotSharp.Plugin.MessageQueue.Interfaces; +using Polly; +using Polly.Retry; using RabbitMQ.Client; using RabbitMQ.Client.Events; -using System.IO; +using System.Runtime; using System.Threading; -namespace BotSharp.Plugin.MessageQueue.Connections; +namespace BotSharp.Plugin.RabbitMQ.Connections; -public class MQConnection : IMQConnection +public class RabbitMQConnection : IRabbitMQConnection { + private readonly RabbitMQSettings _settings; private readonly IConnectionFactory _connectionFactory; private readonly SemaphoreSlim _lock = new(initialCount: 1, maxCount: 1); - private readonly ILogger _logger; + private readonly ILogger _logger; private IConnection _connection; private bool _disposed = false; - public MQConnection( - MessageQueueSettings settings, - ILogger logger) + public RabbitMQConnection( + RabbitMQSettings settings, + ILogger logger) { + _settings = settings; _logger = logger; _connectionFactory = new ConnectionFactory { @@ -55,7 +58,12 @@ public async Task ConnectAsync() return true; } - _connection = await _connectionFactory.CreateConnectionAsync(); + var policy = BuildRegryPolicy(); + await policy.Execute(async () => + { + _connection = await _connectionFactory.CreateConnectionAsync(); + }); + if (IsConnected) { _connection.ConnectionShutdownAsync += OnConnectionShutdownAsync; @@ -74,6 +82,17 @@ public async Task ConnectAsync() } + private RetryPolicy BuildRegryPolicy() + { + return Policy.Handle().WaitAndRetry( + _settings.RetryCount, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (ex, time) => + { + _logger.LogError(ex, $"RabbitMQ cannot build connection: after {time.TotalSeconds:n1}s"); + }); + } + private Task OnConnectionShutdownAsync(object sender, ShutdownEventArgs e) { if (_disposed) diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs new file mode 100644 index 000000000..3e0c70c41 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs @@ -0,0 +1,51 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; + +namespace BotSharp.Plugin.RabbitMQ.Consumers; + +/// +/// Abstract base class for RabbitMQ consumers. +/// Implements IMQConsumer to allow other projects to define consumers independently of RabbitMQ. +/// The RabbitMQ-specific infrastructure is handled by RabbitMQService. +/// +public abstract class MQConsumerBase : IMQConsumer +{ + protected readonly IServiceProvider _services; + protected readonly ILogger _logger; + private bool _disposed = false; + + /// + /// Gets the consumer options for this consumer. + /// Override this property to customize exchange, queue and routing configuration. + /// + public abstract MQConsumerOptions Options { get; } + + protected MQConsumerBase( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + /// + /// Handles the received message from the queue. + /// + /// The consumer channel identifier + /// The message data as string + /// True if the message was handled successfully, false otherwise + public abstract Task HandleMessageAsync(string channel, string data); + + public void Dispose() + { + if (_disposed) + { + return; + } + + var consumerName = GetType().Name; + _logger.LogWarning($"Disposing consumer: {consumerName}"); + _disposed = true; + GC.SuppressFinalize(this); + } +} + diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs new file mode 100644 index 000000000..87dea7979 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -0,0 +1,27 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; + +namespace BotSharp.Plugin.RabbitMQ.Consumers; + +public class ScheduledMessageConsumer : MQConsumerBase +{ + public override MQConsumerOptions Options => new() + { + ExchangeName = "scheduled.exchange", + QueueName = "scheduled.queue", + RoutingKey = "scheduled.routing" + }; + + public ScheduledMessageConsumer( + IServiceProvider services, + ILogger logger) + : base(services, logger) + { + } + + public override async Task HandleMessageAsync(string channel, string data) + { + _logger.LogCritical($"Received delayed message data: {data}"); + return await Task.FromResult(true); + } +} + diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs similarity index 75% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs rename to src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs index 571a0430c..85eef6bc2 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Controllers/MessageQueueController.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs @@ -1,25 +1,24 @@ -using BotSharp.Plugin.MessageQueue.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace BotSharp.Plugin.MessageQueue.Controllers; +namespace BotSharp.Plugin.RabbitMQ.Controllers; /// /// Controller for publishing delayed messages to the message queue /// [Authorize] [ApiController] -public class MessageQueueController : ControllerBase +public class RabbitMQController : ControllerBase { private readonly IServiceProvider _services; private readonly IMQService _mqService; - private readonly ILogger _logger; + private readonly ILogger _logger; - public MessageQueueController( + public RabbitMQController( IServiceProvider services, IMQService mqService, - ILogger logger) + ILogger logger) { _services = services; _mqService = mqService; @@ -48,11 +47,13 @@ public async Task PublishScheduledMessage([FromBody] PublishSched var success = await _mqService.PublishAsync( payload, - exchange: "scheduled.exchange", - routingkey: "scheduled.routing", - milliseconds: request.DelayMilliseconds ?? 10000, - messageId: request.MessageId ?? Guid.NewGuid().ToString()); - + options: new() + { + Exchange = "scheduled.exchange", + RoutingKey = "scheduled.routing", + MilliSeconds = request.DelayMilliseconds ?? 10000, + MessageId = request.MessageId + }); return Ok(); } catch (Exception ex) diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs similarity index 57% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs rename to src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs index 2f65c26c3..c5266d630 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Interfaces/IMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs @@ -1,8 +1,8 @@ using RabbitMQ.Client; -namespace BotSharp.Plugin.MessageQueue.Interfaces; +namespace BotSharp.Plugin.RabbitMQ.Interfaces; -public interface IMQConnection : IDisposable +public interface IRabbitMQConnection : IDisposable { bool IsConnected { get; } Task CreateChannelAsync(); diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs similarity index 95% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs rename to src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs index 6d51cc10f..0897409e7 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Models/PublishDelayedMessageRequest.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.MessageQueue.Models; +namespace BotSharp.Plugin.RabbitMQ.Models; /// /// Request model for publishing a scheduled message diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/ScheduledMessagePayload.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/ScheduledMessagePayload.cs new file mode 100644 index 000000000..2180fb2d7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/ScheduledMessagePayload.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Plugin.RabbitMQ.Models; + +/// +/// Payload for scheduled/delayed messages +/// +public class ScheduledMessagePayload +{ + public string Name { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs new file mode 100644 index 000000000..17d79d7a2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -0,0 +1,51 @@ +using BotSharp.Plugin.RabbitMQ.Connections; +using BotSharp.Plugin.RabbitMQ.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Plugin.RabbitMQ; + +public class RabbitMQPlugin : IBotSharpAppPlugin +{ + public string Id => "3f93407f-3c37-4e25-be28-142a2da9b514"; + public string Name => "RabbitMQ"; + public string Description => "Handle AI messages in RabbitMQ."; + public string IconUrl => "https://icon-library.com/images/message-queue-icon/message-queue-icon-13.jpg"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + var settings = new RabbitMQSettings(); + config.Bind("RabbitMQ", settings); + services.AddSingleton(settings); + + var mqSettings = new MessageQueueSettings(); + config.Bind("MessageQueue", mqSettings); + + if (mqSettings.Enabled && mqSettings.Provider.IsEqualTo("RabbitMQ")) + { + services.AddSingleton(); + services.AddSingleton(); + } + } + + public void Configure(IApplicationBuilder app) + { +#if DEBUG + var sp = app.ApplicationServices; + var mqSettings = sp.GetRequiredService(); + + if (mqSettings.Enabled && mqSettings.Provider.IsEqualTo("RabbitMQ")) + { + var mqService = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + + // Create and subscribe the consumer using the abstract interface + var consumer = new ScheduledMessageConsumer(sp, logger); + mqService.SubscribeAsync(nameof(ScheduledMessageConsumer), consumer) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } +#endif + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs new file mode 100644 index 000000000..6217dda59 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -0,0 +1,235 @@ +using Polly; +using Polly.Retry; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Collections.Concurrent; + +namespace BotSharp.Plugin.RabbitMQ.Services; + +public class RabbitMQService : IMQService +{ + private readonly IRabbitMQConnection _mqConnection; + private readonly RabbitMQSettings _settings; + private readonly ILogger _logger; + + private static readonly ConcurrentDictionary _consumers = []; + + public RabbitMQService( + IRabbitMQConnection mqConnection, + RabbitMQSettings settings, + ILogger logger) + { + _mqConnection = mqConnection; + _settings = settings; + _logger = logger; + } + + public async Task SubscribeAsync(string key, IMQConsumer consumer) + { + if (_consumers.ContainsKey(key)) + { + _logger.LogWarning($"Consumer with key '{key}' is already subscribed."); + return; + } + + var registration = await CreateConsumerRegistrationAsync(consumer); + if (_consumers.TryAdd(key, registration)) + { + _logger.LogInformation($"Consumer '{key}' subscribed to queue '{consumer.Options.QueueName}'."); + } + } + + public async Task UnsubscribeAsync(string key) + { + if (_consumers.TryRemove(key, out var registration)) + { + try + { + if (registration.Channel != null) + { + registration.Channel.Dispose(); + } + registration.Consumer.Dispose(); + _logger.LogInformation($"Consumer '{key}' unsubscribed."); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error unsubscribing consumer '{key}'."); + } + } + } + + private async Task CreateConsumerRegistrationAsync(IMQConsumer consumer) + { + var channel = await CreateChannelAsync(consumer); + + var options = consumer.Options; + var registration = new ConsumerRegistration(consumer, channel); + + var asyncConsumer = new AsyncEventingBasicConsumer(channel); + asyncConsumer.ReceivedAsync += async (sender, eventArgs) => + { + await ConsumeEventAsync(registration, eventArgs); + }; + + await channel.BasicConsumeAsync( + queue: options.QueueName, + autoAck: options.AutoAck, + consumer: asyncConsumer); + + _logger.LogWarning($"RabbitMQ consuming queue '{options.QueueName}'."); + return registration; + } + + private async Task CreateChannelAsync(IMQConsumer consumer) + { + if (!_mqConnection.IsConnected) + { + await _mqConnection.ConnectAsync(); + } + + var options = consumer.Options; + var channel = await _mqConnection.CreateChannelAsync(); + _logger.LogWarning($"Created RabbitMQ channel {channel.ChannelNumber} for queue '{options.QueueName}'"); + + var args = new Dictionary + { + ["x-delayed-type"] = "direct" + }; + + if (options.Arguments != null) + { + foreach (var kvp in options.Arguments) + { + args[kvp.Key] = kvp.Value; + } + } + + await channel.ExchangeDeclareAsync( + exchange: options.ExchangeName, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); + + await channel.QueueDeclareAsync( + queue: options.QueueName, + durable: true, + exclusive: false, + autoDelete: false); + + await channel.QueueBindAsync( + queue: options.QueueName, + exchange: options.ExchangeName, + routingKey: options.RoutingKey); + + return channel; + } + + private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDeliverEventArgs eventArgs) + { + var data = string.Empty; + var options = registration.Consumer.Options; + + try + { + data = Encoding.UTF8.GetString(eventArgs.Body.Span); + _logger.LogInformation($"Message received on '{options.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); + + await registration.Consumer.HandleMessageAsync(options.QueueName, data); + + if (!options.AutoAck && registration.Channel != null) + { + await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error consuming message on queue '{options.QueueName}': {data}"); + if (!options.AutoAck && registration.Channel != null) + { + await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); + } + } + } + + public async Task PublishAsync(T payload, MQPublishOptions options) + { + if (!_mqConnection.IsConnected) + { + await _mqConnection.ConnectAsync(); + } + + var policy = BuildRegryPolicy(); + await policy.Execute(async () => + { + await using var channel = await _mqConnection.CreateChannelAsync(); + var args = new Dictionary + { + ["x-delayed-type"] = "direct" + }; + + await channel.ExchangeDeclareAsync( + exchange: options.Exchange, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); + + var messageId = options.MessageId ?? Guid.NewGuid().ToString(); + var message = new MQMessage(payload, messageId); + var body = ConvertToBinary(message); + var properties = new BasicProperties + { + MessageId = messageId, + DeliveryMode = DeliveryModes.Persistent, + Headers = new Dictionary + { + ["x-delay"] = options.MilliSeconds + } + }; + + await channel.BasicPublishAsync( + exchange: options.Exchange, + routingKey: options.RoutingKey, + mandatory: true, + basicProperties: properties, + body: body); + }); + + return true; + } + + private RetryPolicy BuildRegryPolicy() + { + return Policy.Handle().WaitAndRetry( + _settings.RetryCount, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (ex, time) => + { + _logger.LogError(ex, $"RabbitMQ publish error: after {time.TotalSeconds:n1}s"); + }); + } + + private byte[] ConvertToBinary(T data) + { + var jsonStr = JsonSerializer.Serialize(data); + var body = Encoding.UTF8.GetBytes(jsonStr); + return body; + } + + /// + /// Internal class to track consumer registrations with their RabbitMQ channels. + /// + private class ConsumerRegistration + { + public IMQConsumer Consumer { get; } + public IChannel? Channel { get; } + + public ConsumerRegistration(IMQConsumer consumer, IChannel? channel) + { + Consumer = consumer; + Channel = channel; + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs similarity index 51% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs rename to src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs index 6d3ba0707..0a9c47f23 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Settings/MessageQueueSettings.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs @@ -1,6 +1,6 @@ -namespace BotSharp.Plugin.MessageQueue.Settings; +namespace BotSharp.Plugin.RabbitMQ.Settings; -public class MessageQueueSettings +public class RabbitMQSettings { public string HostName { get; set; } = "localhost"; public int Port { get; set; } = 5672; @@ -8,8 +8,5 @@ public class MessageQueueSettings public string Password { get; set; } = "guest"; public string VirtualHost { get; set; } = "/"; - /// - /// Enable the message queue consumers for delayed message handling - /// - public bool EnableConsumers { get; set; } = false; + public int RetryCount { get; set; } = 5; } diff --git a/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs similarity index 79% rename from src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs rename to src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs index 637062e8b..508d3a4ea 100644 --- a/src/Plugins/BotSharp.Plugin.MessageQueue/Using.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs @@ -28,8 +28,10 @@ global using BotSharp.Abstraction.Messaging.Models.RichContent; global using BotSharp.Abstraction.Options; global using BotSharp.Abstraction.Models; +global using BotSharp.Abstraction.Infrastructures.MessageQueues; +global using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; -global using BotSharp.Plugin.MessageQueue.Settings; -global using BotSharp.Plugin.MessageQueue.Consumers; -global using BotSharp.Plugin.MessageQueue.Models; -global using BotSharp.Plugin.MessageQueue.Controllers; +global using BotSharp.Plugin.RabbitMQ.Settings; +global using BotSharp.Plugin.RabbitMQ.Models; +global using BotSharp.Plugin.RabbitMQ.Interfaces; +global using BotSharp.Plugin.RabbitMQ.Consumers; \ No newline at end of file diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 7a56f5956..be332a38e 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -39,10 +39,10 @@ - + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 84caee211..498f896bb 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1021,11 +1021,17 @@ }, "MessageQueue": { + "Enabled": false, + "Provider": "RabbitMQ" + }, + + "RabbitMQ": { "HostName": "localhost", "Port": 5672, "UserName": "guest", "Password": "guest", - "VirtualHost": "/" + "VirtualHost": "/", + "RetryCount": 5 }, "PluginLoader": { @@ -1072,7 +1078,7 @@ "BotSharp.Plugin.FuzzySharp", "BotSharp.Plugin.MMPEmbedding", "BotSharp.Plugin.MultiTenancy", - "BotSharp.Plugin.MessageQueue" + "BotSharp.Plugin.RabbitMQ" ] }, From 26c3b42d5b267086c2268530d27db9fca51bc2c5 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 20 Jan 2026 00:06:48 -0600 Subject: [PATCH 09/36] minor change --- .../MessageQueues/Models/MQPublishOptions.cs | 1 + .../BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs index e0eba68be..a71df3574 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs @@ -6,4 +6,5 @@ public class MQPublishOptions public string RoutingKey { get; set; } public long MilliSeconds { get; set; } public string? MessageId { get; set; } + public Dictionary Arguments { get; set; } = new(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 6217dda59..3da8a6e6f 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -169,6 +169,14 @@ await policy.Execute(async () => ["x-delayed-type"] = "direct" }; + if (options.Arguments != null) + { + foreach (var kvp in options.Arguments) + { + args[kvp.Key] = kvp.Value; + } + } + await channel.ExchangeDeclareAsync( exchange: options.Exchange, type: "x-delayed-message", From 3380b89c2bbe28ea514acf1ea019423163646404 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 20 Jan 2026 14:01:54 -0600 Subject: [PATCH 10/36] add error handling --- .../Connections/RabbitMQConnection.cs | 1 - .../Consumers/ScheduledMessageConsumer.cs | 2 - .../Services/RabbitMQService.cs | 132 ++++++++++-------- 3 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs index ab7055cfd..2a8896bcf 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs @@ -2,7 +2,6 @@ using Polly.Retry; using RabbitMQ.Client; using RabbitMQ.Client.Events; -using System.Runtime; using System.Threading; namespace BotSharp.Plugin.RabbitMQ.Connections; diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs index 87dea7979..a21aedca8 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; - namespace BotSharp.Plugin.RabbitMQ.Consumers; public class ScheduledMessageConsumer : MQConsumerBase diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 3da8a6e6f..686830615 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -33,7 +33,7 @@ public async Task SubscribeAsync(string key, IMQConsumer consumer) } var registration = await CreateConsumerRegistrationAsync(consumer); - if (_consumers.TryAdd(key, registration)) + if (registration != null && _consumers.TryAdd(key, registration)) { _logger.LogInformation($"Consumer '{key}' subscribed to queue '{consumer.Options.QueueName}'."); } @@ -59,26 +59,34 @@ public async Task UnsubscribeAsync(string key) } } - private async Task CreateConsumerRegistrationAsync(IMQConsumer consumer) + private async Task CreateConsumerRegistrationAsync(IMQConsumer consumer) { - var channel = await CreateChannelAsync(consumer); + try + { + var channel = await CreateChannelAsync(consumer); - var options = consumer.Options; - var registration = new ConsumerRegistration(consumer, channel); + var options = consumer.Options; + var registration = new ConsumerRegistration(consumer, channel); - var asyncConsumer = new AsyncEventingBasicConsumer(channel); - asyncConsumer.ReceivedAsync += async (sender, eventArgs) => - { - await ConsumeEventAsync(registration, eventArgs); - }; + var asyncConsumer = new AsyncEventingBasicConsumer(channel); + asyncConsumer.ReceivedAsync += async (sender, eventArgs) => + { + await ConsumeEventAsync(registration, eventArgs); + }; - await channel.BasicConsumeAsync( - queue: options.QueueName, - autoAck: options.AutoAck, - consumer: asyncConsumer); + await channel.BasicConsumeAsync( + queue: options.QueueName, + autoAck: options.AutoAck, + consumer: asyncConsumer); - _logger.LogWarning($"RabbitMQ consuming queue '{options.QueueName}'."); - return registration; + _logger.LogWarning($"RabbitMQ consuming queue '{options.QueueName}'."); + return registration; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when register consumer in RabbitMQ."); + return null; + } } private async Task CreateChannelAsync(IMQConsumer consumer) @@ -155,57 +163,65 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel public async Task PublishAsync(T payload, MQPublishOptions options) { - if (!_mqConnection.IsConnected) - { - await _mqConnection.ConnectAsync(); - } - - var policy = BuildRegryPolicy(); - await policy.Execute(async () => + try { - await using var channel = await _mqConnection.CreateChannelAsync(); - var args = new Dictionary + if (!_mqConnection.IsConnected) { - ["x-delayed-type"] = "direct" - }; + await _mqConnection.ConnectAsync(); + } - if (options.Arguments != null) + var policy = BuildRegryPolicy(); + await policy.Execute(async () => { - foreach (var kvp in options.Arguments) + await using var channel = await _mqConnection.CreateChannelAsync(); + var args = new Dictionary { - args[kvp.Key] = kvp.Value; - } - } + ["x-delayed-type"] = "direct" + }; - await channel.ExchangeDeclareAsync( - exchange: options.Exchange, - type: "x-delayed-message", - durable: true, - autoDelete: false, - arguments: args); - - var messageId = options.MessageId ?? Guid.NewGuid().ToString(); - var message = new MQMessage(payload, messageId); - var body = ConvertToBinary(message); - var properties = new BasicProperties - { - MessageId = messageId, - DeliveryMode = DeliveryModes.Persistent, - Headers = new Dictionary + if (options.Arguments != null) { - ["x-delay"] = options.MilliSeconds + foreach (var kvp in options.Arguments) + { + args[kvp.Key] = kvp.Value; + } } - }; - await channel.BasicPublishAsync( - exchange: options.Exchange, - routingKey: options.RoutingKey, - mandatory: true, - basicProperties: properties, - body: body); - }); - - return true; + await channel.ExchangeDeclareAsync( + exchange: options.Exchange, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); + + var messageId = options.MessageId ?? Guid.NewGuid().ToString(); + var message = new MQMessage(payload, messageId); + var body = ConvertToBinary(message); + var properties = new BasicProperties + { + MessageId = messageId, + DeliveryMode = DeliveryModes.Persistent, + Headers = new Dictionary + { + ["x-delay"] = options.MilliSeconds + } + }; + + await channel.BasicPublishAsync( + exchange: options.Exchange, + routingKey: options.RoutingKey, + mandatory: true, + basicProperties: properties, + body: body); + }); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when RabbitMQ publish message."); + return false; + } } private RetryPolicy BuildRegryPolicy() From 5c502050af9978610bdbcf57fe6bee1bbafd2860 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 20 Jan 2026 15:05:05 -0600 Subject: [PATCH 11/36] minor change --- .../Infrastructures/MessageQueues}/MQConsumerBase.cs | 2 ++ 1 file changed, 2 insertions(+) rename src/{Plugins/BotSharp.Plugin.RabbitMQ/Consumers => Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues}/MQConsumerBase.cs (94%) diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs similarity index 94% rename from src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs rename to src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index 3e0c70c41..e38c26d9b 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -1,4 +1,6 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues; using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; +using Microsoft.Extensions.Logging; namespace BotSharp.Plugin.RabbitMQ.Consumers; From caba30c375afb1b2336bd8a3961cae8f2b6e6f9e Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 20 Jan 2026 21:20:59 -0600 Subject: [PATCH 12/36] temp save --- .../Agents/Models/AgentRule.cs | 34 ++++++ .../BotSharp.Abstraction/Rules/IRuleAction.cs | 1 + .../Rules/Options/RuleTriggerOptions.cs | 21 ++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 109 ++++++++++++++---- .../Connections/RabbitMQConnection.cs | 4 +- .../Services/RabbitMQService.cs | 4 +- 6 files changed, 144 insertions(+), 29 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 75c0985a8..a390c1c27 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -10,4 +10,38 @@ public class AgentRule [JsonPropertyName("criteria")] public string Criteria { get; set; } = string.Empty; + + [JsonPropertyName("delay")] + public RuleDelay? Delay { get; set; } + + +} + +public class RuleDelay +{ + public int Quantity { get; set; } + public string Unit { get; set; } + + public TimeSpan? Parse() + { + TimeSpan? ts = null; + + switch (Unit) + { + case "seconds": + ts = TimeSpan.FromSeconds(Quantity); + break; + case "minutes": + ts = TimeSpan.FromMinutes(Quantity); + break; + case "hours": + ts = TimeSpan.FromHours(Quantity); + break; + case "days": + ts = TimeSpan.FromDays(Quantity); + break; + } + + return ts; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index bebac5d6f..92f880494 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -2,4 +2,5 @@ namespace BotSharp.Abstraction.Rules; public interface IRuleAction { + Task ExecuteAsync(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 068052b0b..ed09b8ce2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,8 +1,15 @@ +using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; using System.Text.Json; namespace BotSharp.Abstraction.Rules.Options; public class RuleTriggerOptions +{ + public CriteriaOptions? Criteria { get; set; } + public DelayMessageOptions? DelayMessage { get; set; } +} + +public class CriteriaOptions { /// /// Code processor provider @@ -24,3 +31,17 @@ public class RuleTriggerOptions /// public JsonDocument? ArgumentContent { get; set; } } + +public class DelayMessageOptions +{ + public string Payload { get; set; } + public string Exchange { get; set; } + public string RoutingKey { get; set; } + public string? MessageId { get; set; } + public Dictionary Arguments { get; set; } = new(); + + public override string ToString() + { + return $"{Exchange}-{RoutingKey} => {Payload}"; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 5f68b722d..6a3ce4e3d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -7,6 +7,7 @@ using BotSharp.Abstraction.Coding.Utils; using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Hooks; +using BotSharp.Abstraction.Infrastructures.MessageQueues; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Rules.Options; @@ -52,51 +53,42 @@ public async Task> Triggered(IRuleTrigger trigger, string te foreach (var agent in filteredAgents) { // Code trigger - if (options != null) + if (options?.Criteria != null) { - var isTriggered = await TriggerCodeScript(agent, trigger.Name, options); + var isTriggered = await TriggerCodeScript(agent, trigger.Name, options.Criteria); if (!isTriggered) { continue; } } - var convService = _services.GetRequiredService(); - var conv = await convService.NewConversation(new Conversation + var foundTrigger = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); + if (foundTrigger == null) { - Channel = trigger.Channel, - Title = text, - AgentId = agent.Id - }); - - var message = new RoleDialogModel(AgentRole.User, text); - - var allStates = new List - { - new("channel", trigger.Channel) - }; + continue; + } - if (states != null) + if (options?.DelayMessage != null) { - allStates.AddRange(states); + var mqResponse = await SendDelayedMessage(foundTrigger.Delay, options.DelayMessage); + if (mqResponse.HasValue) + { + continue; + } } - await convService.SetConversationId(conv.Id, allStates); + // chat, http request - await convService.SendMessage(agent.Id, - message, - null, - msg => Task.CompletedTask); - await convService.SaveStates(); - newConversationIds.Add(conv.Id); + var conversationId = await RunChat(agent, trigger, text, states); + newConversationIds.Add(conversationId); } return newConversationIds; } #region Private methods - private async Task TriggerCodeScript(Agent agent, string triggerName, RuleTriggerOptions options) + private async Task TriggerCodeScript(Agent agent, string triggerName, CriteriaOptions options) { if (string.IsNullOrWhiteSpace(agent?.Id)) { @@ -200,5 +192,72 @@ private List BuildArguments(string? name, JsonDocument? args) } return keyValues; } + + private async Task SendDelayedMessage(RuleDelay? delay, DelayMessageOptions options) + { + var mqService = _services.GetService(); + if (mqService == null) + { + return null; + } + + if (delay == null || delay.Quantity <= 0) + { + return null; + } + + var ts = delay.Parse(); + if (!ts.HasValue) + { + return null; + } + + _logger.LogWarning($"Start sending delay message {options}"); + var isSent = await mqService.PublishAsync(options.Payload, options: new() + { + Exchange = options.Exchange, + RoutingKey = options.RoutingKey, + MessageId = options.MessageId, + MilliSeconds = (long)ts.Value.TotalMilliseconds, + Arguments = options.Arguments + }); + _logger.LogWarning($"Complete sending delay message: {(isSent ? "Success" : "Failed")}"); + + return isSent; + } + + public async Task RunChat(Agent agent, IRuleTrigger trigger, string text, IEnumerable? states) + { + var convService = _services.GetRequiredService(); + var conv = await convService.NewConversation(new Conversation + { + Channel = trigger.Channel, + Title = text, + AgentId = agent.Id + }); + + var message = new RoleDialogModel(AgentRole.User, text); + + var allStates = new List + { + new("channel", trigger.Channel) + }; + + if (states != null) + { + allStates.AddRange(states); + } + + await convService.SetConversationId(conv.Id, allStates); + + await convService.SendMessage(agent.Id, + message, + null, + msg => Task.CompletedTask); + + await convService.SaveStates(); + + return conv.Id; + } #endregion } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs index 2a8896bcf..10f35eb87 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs @@ -57,7 +57,7 @@ public async Task ConnectAsync() return true; } - var policy = BuildRegryPolicy(); + var policy = BuildRetryPolicy(); await policy.Execute(async () => { _connection = await _connectionFactory.CreateConnectionAsync(); @@ -81,7 +81,7 @@ await policy.Execute(async () => } - private RetryPolicy BuildRegryPolicy() + private RetryPolicy BuildRetryPolicy() { return Policy.Handle().WaitAndRetry( _settings.RetryCount, diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 686830615..ad3f3f859 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -170,7 +170,7 @@ public async Task PublishAsync(T payload, MQPublishOptions options) await _mqConnection.ConnectAsync(); } - var policy = BuildRegryPolicy(); + var policy = BuildRetryPolicy(); await policy.Execute(async () => { await using var channel = await _mqConnection.CreateChannelAsync(); @@ -224,7 +224,7 @@ await channel.BasicPublishAsync( } } - private RetryPolicy BuildRegryPolicy() + private RetryPolicy BuildRetryPolicy() { return Policy.Handle().WaitAndRetry( _settings.RetryCount, From 4a01a6f1bbae5dd3f3a021caadde628ea8b4fff8 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 21 Jan 2026 15:44:39 -0600 Subject: [PATCH 13/36] refine queue message handling --- Directory.Packages.props | 2 +- .../MessageQueues/IMQService.cs | 10 +-- .../Connections/RabbitMQConnection.cs | 3 +- .../Consumers/DummyMessageConsumer.cs | 24 ++++++ .../Consumers/ScheduledMessageConsumer.cs | 6 +- .../Controllers/RabbitMQController.cs | 38 ++++++++-- .../Models/PublishDelayedMessageRequest.cs | 15 ---- .../Models/UnsubscribeConsumerRequest.cs | 6 ++ .../RabbitMQPlugin.cs | 13 +++- .../Services/RabbitMQService.cs | 74 ++++++++++++++----- .../Settings/RabbitMQSettings.cs | 2 - src/WebStarter/appsettings.json | 3 +- 12 files changed, 137 insertions(+), 59 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Models/UnsubscribeConsumerRequest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8e19e4dcb..96897fb92 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs index e77878582..672e539c1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQService.cs @@ -2,7 +2,7 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues; -public interface IMQService +public interface IMQService : IDisposable { /// /// Subscribe a consumer to the message queue. @@ -10,15 +10,15 @@ public interface IMQService /// /// Unique identifier for the consumer /// The consumer implementing IMQConsumer interface - /// Task representing the async subscription operation - Task SubscribeAsync(string key, IMQConsumer consumer); + /// Task representing the async subscription operation + Task SubscribeAsync(string key, IMQConsumer consumer); /// /// Unsubscribe a consumer from the message queue. /// /// Unique identifier for the consumer - /// Task representing the async unsubscription operation - Task UnsubscribeAsync(string key); + /// Task representing the async unsubscription operation + Task UnsubscribeAsync(string key); /// /// Publish payload to message queue diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs index 10f35eb87..c2782e9fe 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs @@ -12,6 +12,7 @@ public class RabbitMQConnection : IRabbitMQConnection private readonly IConnectionFactory _connectionFactory; private readonly SemaphoreSlim _lock = new(initialCount: 1, maxCount: 1); private readonly ILogger _logger; + private readonly int _retryCount = 5; private IConnection _connection; private bool _disposed = false; @@ -84,7 +85,7 @@ await policy.Execute(async () => private RetryPolicy BuildRetryPolicy() { return Policy.Handle().WaitAndRetry( - _settings.RetryCount, + _retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => { diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs new file mode 100644 index 000000000..4ce9282cb --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs @@ -0,0 +1,24 @@ +namespace BotSharp.Plugin.RabbitMQ.Consumers; + +public class DummyMessageConsumer : MQConsumerBase +{ + public override MQConsumerOptions Options => new() + { + ExchangeName = "my.exchange", + QueueName = "dummy.queue", + RoutingKey = "my.routing" + }; + + public DummyMessageConsumer( + IServiceProvider services, + ILogger logger) + : base(services, logger) + { + } + + public override async Task HandleMessageAsync(string channel, string data) + { + _logger.LogCritical($"Received delayed dummy message data: {data}"); + return await Task.FromResult(true); + } +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs index a21aedca8..b2deb177e 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -4,9 +4,9 @@ public class ScheduledMessageConsumer : MQConsumerBase { public override MQConsumerOptions Options => new() { - ExchangeName = "scheduled.exchange", + ExchangeName = "my.exchange", QueueName = "scheduled.queue", - RoutingKey = "scheduled.routing" + RoutingKey = "my.routing" }; public ScheduledMessageConsumer( @@ -18,7 +18,7 @@ public ScheduledMessageConsumer( public override async Task HandleMessageAsync(string channel, string data) { - _logger.LogCritical($"Received delayed message data: {data}"); + _logger.LogCritical($"Received delayed scheduled message data: {data}"); return await Task.FromResult(true); } } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs index 85eef6bc2..be9a3d834 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs @@ -4,9 +4,6 @@ namespace BotSharp.Plugin.RabbitMQ.Controllers; -/// -/// Controller for publishing delayed messages to the message queue -/// [Authorize] [ApiController] public class RabbitMQController : ControllerBase @@ -29,8 +26,7 @@ public RabbitMQController( /// Publish a scheduled message to be delivered after a delay /// /// The scheduled message request - /// Publish result with message ID and expected delivery time - [HttpPost("/message-queue/scheduled")] + [HttpPost("/message-queue/publish")] public async Task PublishScheduledMessage([FromBody] PublishScheduledMessageRequest request) { if (request == null) @@ -49,12 +45,12 @@ public async Task PublishScheduledMessage([FromBody] PublishSched payload, options: new() { - Exchange = "scheduled.exchange", - RoutingKey = "scheduled.routing", + Exchange = "my.exchange", + RoutingKey = "my.routing", MilliSeconds = request.DelayMilliseconds ?? 10000, MessageId = request.MessageId }); - return Ok(); + return Ok(new { Success = success }); } catch (Exception ex) { @@ -63,5 +59,31 @@ public async Task PublishScheduledMessage([FromBody] PublishSched new PublishMessageResponse { Success = false, Error = ex.Message }); } } + + /// + /// Unsubscribe a consumer + /// + /// + /// + [HttpPost("/message-queue/unsubscribe/consumer")] + public async Task UnSubscribeConsuer([FromBody] UnsubscribeConsumerRequest request) + { + if (request == null) + { + return BadRequest(new { Success = false, Error = "Request body is required." }); + } + + try + { + var success = await _mqService.UnsubscribeAsync(request.Name); + return Ok(new { Success = success }); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to unsubscribe consumer {request.Name}"); + return StatusCode(StatusCodes.Status500InternalServerError, + new { Success = false, Error = ex.Message }); + } + } } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs index 0897409e7..ad655b795 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/PublishDelayedMessageRequest.cs @@ -23,21 +23,6 @@ public class PublishMessageResponse /// public bool Success { get; set; } - /// - /// The message ID - /// - public string? MessageId { get; set; } - - /// - /// The calculated delay in milliseconds - /// - public long DelayMilliseconds { get; set; } - - /// - /// The expected delivery time (UTC) - /// - public DateTime ExpectedDeliveryTime { get; set; } - /// /// Error message if publish failed /// diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/UnsubscribeConsumerRequest.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/UnsubscribeConsumerRequest.cs new file mode 100644 index 000000000..509d432b2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/UnsubscribeConsumerRequest.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Plugin.RabbitMQ.Models; + +public class UnsubscribeConsumerRequest +{ + public string Name { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index 17d79d7a2..3a841fcd1 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -2,6 +2,7 @@ using BotSharp.Plugin.RabbitMQ.Services; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace BotSharp.Plugin.RabbitMQ; @@ -37,11 +38,17 @@ public void Configure(IApplicationBuilder app) if (mqSettings.Enabled && mqSettings.Provider.IsEqualTo("RabbitMQ")) { var mqService = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); + var loggerFactory = sp.GetRequiredService(); // Create and subscribe the consumer using the abstract interface - var consumer = new ScheduledMessageConsumer(sp, logger); - mqService.SubscribeAsync(nameof(ScheduledMessageConsumer), consumer) + var scheduledConsumer = new ScheduledMessageConsumer(sp, loggerFactory.CreateLogger()); + mqService.SubscribeAsync(nameof(ScheduledMessageConsumer), scheduledConsumer) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + var dummyConsumer = new DummyMessageConsumer(sp, loggerFactory.CreateLogger()); + mqService.SubscribeAsync(nameof(DummyMessageConsumer), dummyConsumer) .ConfigureAwait(false) .GetAwaiter() .GetResult(); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index ad3f3f859..38a3165c1 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -9,53 +9,59 @@ namespace BotSharp.Plugin.RabbitMQ.Services; public class RabbitMQService : IMQService { private readonly IRabbitMQConnection _mqConnection; - private readonly RabbitMQSettings _settings; private readonly ILogger _logger; + private readonly int _retryCount = 5; + private bool _disposed = false; private static readonly ConcurrentDictionary _consumers = []; public RabbitMQService( IRabbitMQConnection mqConnection, - RabbitMQSettings settings, ILogger logger) { _mqConnection = mqConnection; - _settings = settings; _logger = logger; } - public async Task SubscribeAsync(string key, IMQConsumer consumer) + public async Task SubscribeAsync(string key, IMQConsumer consumer) { if (_consumers.ContainsKey(key)) { _logger.LogWarning($"Consumer with key '{key}' is already subscribed."); - return; + return false; } var registration = await CreateConsumerRegistrationAsync(consumer); if (registration != null && _consumers.TryAdd(key, registration)) { _logger.LogInformation($"Consumer '{key}' subscribed to queue '{consumer.Options.QueueName}'."); + return true; } + + return false; } - public async Task UnsubscribeAsync(string key) + public async Task UnsubscribeAsync(string key) { - if (_consumers.TryRemove(key, out var registration)) + if (!_consumers.TryRemove(key, out var registration)) { - try - { - if (registration.Channel != null) - { - registration.Channel.Dispose(); - } - registration.Consumer.Dispose(); - _logger.LogInformation($"Consumer '{key}' unsubscribed."); - } - catch (Exception ex) + return false; + } + + try + { + if (registration.Channel != null) { - _logger.LogError(ex, $"Error unsubscribing consumer '{key}'."); + registration.Channel.Dispose(); } + registration.Consumer.Dispose(); + _logger.LogInformation($"Consumer '{key}' unsubscribed."); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error unsubscribing consumer '{key}'."); + return false; } } @@ -131,6 +137,17 @@ await channel.QueueBindAsync( exchange: options.ExchangeName, routingKey: options.RoutingKey); + channel.ChannelShutdownAsync += async (sender, eventArgs) => + { + _logger.LogWarning($"RabbitMQ channel shutdown: {eventArgs}"); + + if (!_disposed && _mqConnection.IsConnected) + { + channel.Dispose(); + channel = await CreateChannelAsync(consumer); + } + }; + return channel; } @@ -227,7 +244,7 @@ await channel.BasicPublishAsync( private RetryPolicy BuildRetryPolicy() { return Policy.Handle().WaitAndRetry( - _settings.RetryCount, + _retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => { @@ -242,6 +259,25 @@ private byte[] ConvertToBinary(T data) return body; } + public void Dispose() + { + if (_disposed) + { + return; + } + + _logger.LogWarning($"Disposing {nameof(RabbitMQService)}"); + + foreach (var item in _consumers) + { + item.Value.Consumer?.Dispose(); + item.Value.Channel?.Dispose(); + } + + _disposed = true; + GC.SuppressFinalize(this); + } + /// /// Internal class to track consumer registrations with their RabbitMQ channels. /// diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs index 0a9c47f23..0e61b5c71 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Settings/RabbitMQSettings.cs @@ -7,6 +7,4 @@ public class RabbitMQSettings public string UserName { get; set; } = "guest"; public string Password { get; set; } = "guest"; public string VirtualHost { get; set; } = "/"; - - public int RetryCount { get; set; } = 5; } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 498f896bb..322315192 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1030,8 +1030,7 @@ "Port": 5672, "UserName": "guest", "Password": "guest", - "VirtualHost": "/", - "RetryCount": 5 + "VirtualHost": "/" }, "PluginLoader": { From 292270c4336ab3349b28cc3ae5f81864768ea1a8 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 21 Jan 2026 17:40:50 -0600 Subject: [PATCH 14/36] refine rule criteria and actions --- .../Agents/Models/AgentRule.cs | 13 +- .../Rules/Enums/RuleActionType.cs | 7 + .../Rules/Enums/RuleDelayUnit.cs | 9 + .../BotSharp.Abstraction/Rules/IRuleAction.cs | 13 +- .../Rules/IRuleCriteria.cs | 4 + .../Rules/Models/RuleChatActionPayload.cs | 8 + .../Rules/Options/RuleActionOptions.cs | 14 ++ .../Rules/Options/RuleCriteriaOptions.cs | 34 +++ .../Rules/Options/RuleDelayMessageOptions.cs | 34 +++ .../Rules/Options/RuleTriggerOptions.cs | 43 +--- .../Constants/RuleHandler.cs | 6 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 231 +++--------------- .../BotSharp.Core.Rules/RulesPlugin.cs | 3 + .../Services/RuleAction.Chat.cs | 36 +++ .../Services/RuleAction.Http.cs | 9 + .../Services/RuleAction.Messaging.cs | 38 +++ .../Services/RuleAction.cs | 17 ++ .../Services/RuleCriteria.cs | 127 ++++++++++ .../BotSharp.Core.Rules/Using.cs | 22 +- .../Services/RabbitMQService.cs | 11 - 20 files changed, 421 insertions(+), 258 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index a390c1c27..d8cd66137 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Rules.Enums; + namespace BotSharp.Abstraction.Agents.Models; public class AgentRule @@ -14,7 +16,8 @@ public class AgentRule [JsonPropertyName("delay")] public RuleDelay? Delay { get; set; } - + [JsonPropertyName("action")] + public string? Action { get; set; } } public class RuleDelay @@ -28,16 +31,16 @@ public class RuleDelay switch (Unit) { - case "seconds": + case RuleDelayUnit.Second: ts = TimeSpan.FromSeconds(Quantity); break; - case "minutes": + case RuleDelayUnit.Minute: ts = TimeSpan.FromMinutes(Quantity); break; - case "hours": + case RuleDelayUnit.Hour: ts = TimeSpan.FromHours(Quantity); break; - case "days": + case RuleDelayUnit.Day: ts = TimeSpan.FromDays(Quantity); break; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs new file mode 100644 index 000000000..f7bb0a383 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Rules.Enums; + +public static class RuleActionType +{ + public const string Chat = "chat"; + public const string Http = "http"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs new file mode 100644 index 000000000..cc862e68b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Rules.Enums; + +public static class RuleDelayUnit +{ + public const string Second = "second"; + public const string Minute = "minute"; + public const string Hour = "hour"; + public const string Day = "day"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 92f880494..4b0d541c4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -1,6 +1,17 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules; public interface IRuleAction { - Task ExecuteAsync(); + string Provider { get; } + + Task SendChatAsync(Agent agent, RuleChatActionPayload payload) + => throw new NotImplementedException(); + + Task SendHttpRequestAsync() + => throw new NotImplementedException(); + + Task SendDelayedMessageAsync(RuleDelay delay, RuleDelayMessageOptions options) + => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs index bc5022911..600ebb546 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs @@ -2,4 +2,8 @@ namespace BotSharp.Abstraction.Rules; public interface IRuleCriteria { + string Provider { get; } + + Task ExecuteCriteriaAsync(Agent agent, string triggerName, CriteriaExecuteOptions options) + => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs new file mode 100644 index 000000000..84c22353a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleChatActionPayload +{ + public string Text { get; set; } + public string Channel { get; set; } + public IEnumerable? States { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs new file mode 100644 index 000000000..9701c1a59 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleActionOptions +{ + /// + /// Rule action provider + /// + public string Provider { get; set; } = "botsharp-rule"; + + /// + /// Delay message options + /// + public RuleDelayMessageOptions? DelayMessage { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs new file mode 100644 index 000000000..30c820f97 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs @@ -0,0 +1,34 @@ +using System.Text.Json; + +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleCriteriaOptions : CriteriaExecuteOptions +{ + /// + /// Criteria execution provider + /// + public string Provider { get; set; } = "botsharp-rule"; +} + +public class CriteriaExecuteOptions +{ + /// + /// Code processor provider + /// + public string? CodeProcessor { get; set; } + + /// + /// Code script name + /// + public string? CodeScriptName { get; set; } + + /// + /// Argument name as an input key to the code script + /// + public string? ArgumentName { get; set; } + + /// + /// Json arguments as an input value to the code script + /// + public JsonDocument? ArgumentContent { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs new file mode 100644 index 000000000..944da1081 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs @@ -0,0 +1,34 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleDelayMessageOptions +{ + /// + /// Message payload + /// + public string Payload { get; set; } + + /// + /// Exchange + /// + public string Exchange { get; set; } + + /// + /// Routing key + /// + public string RoutingKey { get; set; } + + /// + /// Delayed message id + /// + public string? MessageId { get; set; } + + /// + /// Arguments + /// + public Dictionary Arguments { get; set; } = new(); + + public override string ToString() + { + return $"{Exchange}-{RoutingKey} => {Payload}"; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index ed09b8ce2..1eb07c86c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,47 +1,8 @@ -using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; -using System.Text.Json; - namespace BotSharp.Abstraction.Rules.Options; public class RuleTriggerOptions { - public CriteriaOptions? Criteria { get; set; } - public DelayMessageOptions? DelayMessage { get; set; } + public RuleCriteriaOptions? Criteria { get; set; } + public RuleActionOptions? Action { get; set; } } -public class CriteriaOptions -{ - /// - /// Code processor provider - /// - public string? CodeProcessor { get; set; } - - /// - /// Code script name - /// - public string? CodeScriptName { get; set; } - - /// - /// Argument name as an input key to the code script - /// - public string? ArgumentName { get; set; } - - /// - /// Json arguments as an input value to the code script - /// - public JsonDocument? ArgumentContent { get; set; } -} - -public class DelayMessageOptions -{ - public string Payload { get; set; } - public string Exchange { get; set; } - public string RoutingKey { get; set; } - public string? MessageId { get; set; } - public Dictionary Arguments { get; set; } = new(); - - public override string ToString() - { - return $"{Exchange}-{RoutingKey} => {Payload}"; - } -} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs new file mode 100644 index 000000000..9c35a4139 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Core.Rules.Constants; + +public static class RuleHandler +{ + public const string DefaultProvider = "botsharp-rule"; +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6a3ce4e3d..6ec885e81 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,20 +1,4 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Coding; -using BotSharp.Abstraction.Coding.Contexts; -using BotSharp.Abstraction.Coding.Enums; -using BotSharp.Abstraction.Coding.Models; -using BotSharp.Abstraction.Coding.Settings; -using BotSharp.Abstraction.Coding.Utils; -using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Infrastructures.MessageQueues; -using BotSharp.Abstraction.Models; -using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Rules.Options; -using BotSharp.Abstraction.Utilities; -using Microsoft.Extensions.Logging; using System.Data; -using System.Text.Json; namespace BotSharp.Core.Rules.Engines; @@ -22,16 +6,13 @@ public class RuleEngine : IRuleEngine { private readonly IServiceProvider _services; private readonly ILogger _logger; - private readonly CodingSettings _codingSettings; public RuleEngine( IServiceProvider services, - ILogger logger, - CodingSettings codingSettings) + ILogger logger) { _services = services; _logger = logger; - _codingSettings = codingSettings; } public async Task> Triggered(IRuleTrigger trigger, string text, IEnumerable? states = null, RuleTriggerOptions? options = null) @@ -52,10 +33,18 @@ public async Task> Triggered(IRuleTrigger trigger, string te var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); foreach (var agent in filteredAgents) { - // Code trigger + // Criteria if (options?.Criteria != null) { - var isTriggered = await TriggerCodeScript(agent, trigger.Name, options.Criteria); + var criteria = _services.GetServices() + .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? RuleHandler.DefaultProvider)); + + if (criteria == null) + { + continue; + } + + var isTriggered = await criteria.ExecuteCriteriaAsync(agent, trigger.Name, options.Criteria); if (!isTriggered) { continue; @@ -68,196 +57,40 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - if (options?.DelayMessage != null) + var action = _services.GetServices() + .FirstOrDefault(x => x.Provider == (options?.Action?.Provider ?? RuleHandler.DefaultProvider)); + if (action == null) { - var mqResponse = await SendDelayedMessage(foundTrigger.Delay, options.DelayMessage); - if (mqResponse.HasValue) - { - continue; - } - } - - // chat, http request - - - var conversationId = await RunChat(agent, trigger, text, states); - newConversationIds.Add(conversationId); - } - - return newConversationIds; - } - - #region Private methods - private async Task TriggerCodeScript(Agent agent, string triggerName, CriteriaOptions options) - { - if (string.IsNullOrWhiteSpace(agent?.Id)) - { - return false; - } - - var provider = options.CodeProcessor ?? BuiltInCodeProcessor.PyInterpreter; - var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); - if (processor == null) - { - _logger.LogWarning($"Unable to find code processor: {provider}."); - return false; - } - - var agentService = _services.GetRequiredService(); - var scriptName = options.CodeScriptName ?? $"{triggerName}_rule.py"; - var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - - var msg = $"rule trigger ({triggerName}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; - - if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) - { - _logger.LogWarning($"Unable to find {msg}."); - return false; - } - - try - { - var hooks = _services.GetHooks(agent.Id); - - var arguments = BuildArguments(options.ArgumentName, options.ArgumentContent); - var context = new CodeExecutionContext - { - CodeScript = codeScript, - Arguments = arguments - }; - - foreach (var hook in hooks) - { - await hook.BeforeCodeExecution(agent, context); + continue; } - var (useLock, useProcess, timeoutSeconds) = CodingUtil.GetCodeExecutionConfig(_codingSettings); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - var response = processor.Run(codeScript.Content, options: new() - { - ScriptName = scriptName, - Arguments = arguments, - UseLock = useLock, - UseProcess = useProcess - }, cancellationToken: cts.Token); - - var codeResponse = new CodeExecutionResponseModel + if (options?.Action?.DelayMessage != null) { - CodeProcessor = processor.Provider, - CodeScript = codeScript, - Arguments = arguments.DistinctBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value ?? string.Empty), - ExecutionResult = response - }; - - foreach (var hook in hooks) - { - await hook.AfterCodeExecution(agent, codeResponse); + var isSent = await action.SendDelayedMessageAsync(foundTrigger.Delay, options.Action.DelayMessage); + continue; } - if (response == null || !response.Success) + // Execute action + if (foundTrigger.Action.IsEqualTo(RuleActionType.Http)) { - _logger.LogWarning($"Failed to handle {msg}"); - return false; - } - bool result; - LogLevel logLevel; - if (response.Result.IsEqualTo("true")) - { - logLevel = LogLevel.Information; - result = true; } else { - logLevel = LogLevel.Warning; - result = false; - } - - _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when handling {msg}"); - return false; - } - } - - private List BuildArguments(string? name, JsonDocument? args) - { - var keyValues = new List(); - if (args != null) - { - keyValues.Add(new KeyValue(name ?? "trigger_args", args.RootElement.GetRawText())); - } - return keyValues; - } - - private async Task SendDelayedMessage(RuleDelay? delay, DelayMessageOptions options) - { - var mqService = _services.GetService(); - if (mqService == null) - { - return null; - } - - if (delay == null || delay.Quantity <= 0) - { - return null; - } - - var ts = delay.Parse(); - if (!ts.HasValue) - { - return null; - } - - _logger.LogWarning($"Start sending delay message {options}"); - var isSent = await mqService.PublishAsync(options.Payload, options: new() - { - Exchange = options.Exchange, - RoutingKey = options.RoutingKey, - MessageId = options.MessageId, - MilliSeconds = (long)ts.Value.TotalMilliseconds, - Arguments = options.Arguments - }); - _logger.LogWarning($"Complete sending delay message: {(isSent ? "Success" : "Failed")}"); - - return isSent; - } - - public async Task RunChat(Agent agent, IRuleTrigger trigger, string text, IEnumerable? states) - { - var convService = _services.GetRequiredService(); - var conv = await convService.NewConversation(new Conversation - { - Channel = trigger.Channel, - Title = text, - AgentId = agent.Id - }); - - var message = new RoleDialogModel(AgentRole.User, text); - - var allStates = new List - { - new("channel", trigger.Channel) - }; + var conversationId = await action.SendChatAsync(agent, payload: new() + { + Text = text, + Channel = trigger.Channel, + States = states + }); - if (states != null) - { - allStates.AddRange(states); + if (!string.IsNullOrEmpty(conversationId)) + { + newConversationIds.Add(conversationId); + } + } } - await convService.SetConversationId(conv.Id, allStates); - - await convService.SendMessage(agent.Id, - message, - null, - msg => Task.CompletedTask); - - await convService.SaveStates(); - - return conv.Id; + return newConversationIds; } - #endregion } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 56e1fb8ae..6135da50d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,4 +1,5 @@ using BotSharp.Core.Rules.Engines; +using BotSharp.Core.Rules.Services; namespace BotSharp.Core.Rules; @@ -17,5 +18,7 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs new file mode 100644 index 000000000..c626b350e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs @@ -0,0 +1,36 @@ +namespace BotSharp.Core.Rules.Services; + +public partial class RuleAction : IRuleAction +{ + public async Task SendChatAsync(Agent agent, RuleChatActionPayload payload) + { + var convService = _services.GetRequiredService(); + var conv = await convService.NewConversation(new Conversation + { + Channel = payload.Channel, + Title = payload.Text, + AgentId = agent.Id + }); + + var message = new RoleDialogModel(AgentRole.User, payload.Text); + + var allStates = new List + { + new("channel", payload.Channel) + }; + + if (!payload.States.IsNullOrEmpty()) + { + allStates.AddRange(payload.States!); + } + + await convService.SetConversationId(conv.Id, allStates); + await convService.SendMessage(agent.Id, + message, + null, + msg => Task.CompletedTask); + + await convService.SaveStates(); + return conv.Id; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs new file mode 100644 index 000000000..01590a3be --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Core.Rules.Services; + +public partial class RuleAction +{ + public Task SendHttpRequestAsync() + { + throw new NotImplementedException(); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs new file mode 100644 index 000000000..f64b8f2cb --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs @@ -0,0 +1,38 @@ +namespace BotSharp.Core.Rules.Services; + +public partial class RuleAction +{ + public async Task SendDelayedMessageAsync(RuleDelay delay, RuleDelayMessageOptions options) + { + var mqService = _services.GetService(); + if (mqService == null) + { + return false; + } + + if (delay == null || delay.Quantity < 0) + { + return false; + } + + var ts = delay.Parse(); + if (!ts.HasValue) + { + return false; + } + + _logger.LogWarning($"Start sending delay message {options}"); + + var isSent = await mqService.PublishAsync(options.Payload, options: new() + { + Exchange = options.Exchange, + RoutingKey = options.RoutingKey, + MessageId = options.MessageId, + MilliSeconds = (long)ts.Value.TotalMilliseconds, + Arguments = options.Arguments + }); + + _logger.LogWarning($"Complete sending delay message: {(isSent ? "Success" : "Failed")}"); + return isSent; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs new file mode 100644 index 000000000..0fd271550 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs @@ -0,0 +1,17 @@ +namespace BotSharp.Core.Rules.Services; + +public partial class RuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public RuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Provider => RuleHandler.DefaultProvider; +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs new file mode 100644 index 000000000..2ae06253d --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs @@ -0,0 +1,127 @@ +using System.Text.Json; + +namespace BotSharp.Core.Rules.Services; + +public class RuleCriteria : IRuleCriteria +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly CodingSettings _codingSettings; + + public RuleCriteria( + IServiceProvider services, + ILogger logger, + CodingSettings codingSettings) + { + _services = services; + _logger = logger; + _codingSettings = codingSettings; + } + + public string Provider => RuleHandler.DefaultProvider; + + public async Task ExecuteCriteriaAsync(Agent agent, string triggerName, CriteriaExecuteOptions options) + { + if (string.IsNullOrWhiteSpace(agent?.Id)) + { + return false; + } + + var provider = options.CodeProcessor ?? BuiltInCodeProcessor.PyInterpreter; + var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + if (processor == null) + { + _logger.LogWarning($"Unable to find code processor: {provider}."); + return false; + } + + var agentService = _services.GetRequiredService(); + var scriptName = options.CodeScriptName ?? $"{triggerName}_rule.py"; + var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); + + var msg = $"rule trigger ({triggerName}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; + + if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) + { + _logger.LogWarning($"Unable to find {msg}."); + return false; + } + + try + { + var hooks = _services.GetHooks(agent.Id); + + var arguments = BuildArguments(options.ArgumentName, options.ArgumentContent); + var context = new CodeExecutionContext + { + CodeScript = codeScript, + Arguments = arguments + }; + + foreach (var hook in hooks) + { + await hook.BeforeCodeExecution(agent, context); + } + + var (useLock, useProcess, timeoutSeconds) = CodingUtil.GetCodeExecutionConfig(_codingSettings); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + var response = processor.Run(codeScript.Content, options: new() + { + ScriptName = scriptName, + Arguments = arguments, + UseLock = useLock, + UseProcess = useProcess + }, cancellationToken: cts.Token); + + var codeResponse = new CodeExecutionResponseModel + { + CodeProcessor = processor.Provider, + CodeScript = codeScript, + Arguments = arguments.DistinctBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value ?? string.Empty), + ExecutionResult = response + }; + + foreach (var hook in hooks) + { + await hook.AfterCodeExecution(agent, codeResponse); + } + + if (response == null || !response.Success) + { + _logger.LogWarning($"Failed to handle {msg}"); + return false; + } + + bool result; + LogLevel logLevel; + if (response.Result.IsEqualTo("true")) + { + logLevel = LogLevel.Information; + result = true; + } + else + { + logLevel = LogLevel.Warning; + result = false; + } + + _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when handling {msg}"); + return false; + } + } + + private List BuildArguments(string? name, JsonDocument? args) + { + var keyValues = new List(); + if (args != null) + { + keyValues.Add(new KeyValue(name ?? "trigger_args", args.RootElement.GetRawText())); + } + return keyValues; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index a4353c960..0f999ff88 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -1,5 +1,6 @@ global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Plugins; @@ -8,4 +9,23 @@ global using BotSharp.Abstraction.Instructs; global using BotSharp.Abstraction.Instructs.Models; -global using BotSharp.Abstraction.Rules; \ No newline at end of file +global using BotSharp.Abstraction.Agents.Models; +global using BotSharp.Abstraction.Conversations; + +global using BotSharp.Abstraction.Infrastructures.MessageQueues; +global using BotSharp.Abstraction.Models; +global using BotSharp.Abstraction.Repositories.Filters; +global using BotSharp.Abstraction.Rules; +global using BotSharp.Abstraction.Rules.Enums; +global using BotSharp.Abstraction.Rules.Options; +global using BotSharp.Abstraction.Rules.Models; +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Abstraction.Coding; +global using BotSharp.Abstraction.Coding.Contexts; +global using BotSharp.Abstraction.Coding.Enums; +global using BotSharp.Abstraction.Coding.Models; +global using BotSharp.Abstraction.Coding.Utils; +global using BotSharp.Abstraction.Coding.Settings; +global using BotSharp.Abstraction.Hooks; + +global using BotSharp.Core.Rules.Constants; diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 38a3165c1..872bf2713 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -137,17 +137,6 @@ await channel.QueueBindAsync( exchange: options.ExchangeName, routingKey: options.RoutingKey); - channel.ChannelShutdownAsync += async (sender, eventArgs) => - { - _logger.LogWarning($"RabbitMQ channel shutdown: {eventArgs}"); - - if (!_disposed && _mqConnection.IsConnected) - { - channel.Dispose(); - channel = await CreateChannelAsync(consumer); - } - }; - return channel; } From 33c247ff4ebf47fe7eba04b9d9dcc9bd55aa2a3b Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 21 Jan 2026 17:48:02 -0600 Subject: [PATCH 15/36] rename to messaging --- .../BotSharp.Abstraction/Rules/IRuleAction.cs | 2 +- .../Rules/Options/RuleActionOptions.cs | 2 +- ...sageOptions.cs => RuleMessagingOptions.cs} | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 4 +- .../Services/RuleAction.Chat.cs | 46 +++++++++++-------- .../Services/RuleAction.Messaging.cs | 2 +- 6 files changed, 33 insertions(+), 25 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Rules/Options/{RuleDelayMessageOptions.cs => RuleMessagingOptions.cs} (94%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 4b0d541c4..6c5028904 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -12,6 +12,6 @@ Task SendChatAsync(Agent agent, RuleChatActionPayload payload) Task SendHttpRequestAsync() => throw new NotImplementedException(); - Task SendDelayedMessageAsync(RuleDelay delay, RuleDelayMessageOptions options) + Task SendDelayedMessageAsync(RuleDelay delay, RuleMessagingOptions options) => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs index 9701c1a59..919ed25ef 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs @@ -10,5 +10,5 @@ public class RuleActionOptions /// /// Delay message options /// - public RuleDelayMessageOptions? DelayMessage { get; set; } + public RuleMessagingOptions? Messaging { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMessagingOptions.cs similarity index 94% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMessagingOptions.cs index 944da1081..b3a5d91de 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleDelayMessageOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMessagingOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Rules.Options; -public class RuleDelayMessageOptions +public class RuleMessagingOptions { /// /// Message payload diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6ec885e81..c5d5a0942 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -64,9 +64,9 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - if (options?.Action?.DelayMessage != null) + if (options?.Action?.Messaging != null) { - var isSent = await action.SendDelayedMessageAsync(foundTrigger.Delay, options.Action.DelayMessage); + var isSent = await action.SendDelayedMessageAsync(foundTrigger.Delay, options.Action.Messaging); continue; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs index c626b350e..84925a92d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs @@ -4,33 +4,41 @@ public partial class RuleAction : IRuleAction { public async Task SendChatAsync(Agent agent, RuleChatActionPayload payload) { - var convService = _services.GetRequiredService(); - var conv = await convService.NewConversation(new Conversation + try { - Channel = payload.Channel, - Title = payload.Text, - AgentId = agent.Id - }); + var convService = _services.GetRequiredService(); + var conv = await convService.NewConversation(new Conversation + { + Channel = payload.Channel, + Title = payload.Text, + AgentId = agent.Id + }); - var message = new RoleDialogModel(AgentRole.User, payload.Text); + var message = new RoleDialogModel(AgentRole.User, payload.Text); - var allStates = new List + var allStates = new List { new("channel", payload.Channel) }; - if (!payload.States.IsNullOrEmpty()) - { - allStates.AddRange(payload.States!); - } + if (!payload.States.IsNullOrEmpty()) + { + allStates.AddRange(payload.States!); + } - await convService.SetConversationId(conv.Id, allStates); - await convService.SendMessage(agent.Id, - message, - null, - msg => Task.CompletedTask); + await convService.SetConversationId(conv.Id, allStates); + await convService.SendMessage(agent.Id, + message, + null, + msg => Task.CompletedTask); - await convService.SaveStates(); - return conv.Id; + await convService.SaveStates(); + return conv.Id; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when sending chat via rule action."); + return string.Empty; + } } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs index f64b8f2cb..5cf10db3e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs @@ -2,7 +2,7 @@ namespace BotSharp.Core.Rules.Services; public partial class RuleAction { - public async Task SendDelayedMessageAsync(RuleDelay delay, RuleDelayMessageOptions options) + public async Task SendDelayedMessageAsync(RuleDelay delay, RuleMessagingOptions options) { var mqService = _services.GetService(); if (mqService == null) From a8cdca7ef6f0c9510c0d5166dff10ca0522ea674 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 21 Jan 2026 17:49:37 -0600 Subject: [PATCH 16/36] remove delayed --- src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs | 2 +- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 2 +- .../BotSharp.Core.Rules/Services/RuleAction.Messaging.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 6c5028904..1ee05cf13 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -12,6 +12,6 @@ Task SendChatAsync(Agent agent, RuleChatActionPayload payload) Task SendHttpRequestAsync() => throw new NotImplementedException(); - Task SendDelayedMessageAsync(RuleDelay delay, RuleMessagingOptions options) + Task SendMessageAsync(RuleDelay delay, RuleMessagingOptions options) => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index c5d5a0942..d3512c3a9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -66,7 +66,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te if (options?.Action?.Messaging != null) { - var isSent = await action.SendDelayedMessageAsync(foundTrigger.Delay, options.Action.Messaging); + var isSent = await action.SendMessageAsync(foundTrigger.Delay, options.Action.Messaging); continue; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs index 5cf10db3e..2c50fb432 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs @@ -2,7 +2,7 @@ namespace BotSharp.Core.Rules.Services; public partial class RuleAction { - public async Task SendDelayedMessageAsync(RuleDelay delay, RuleMessagingOptions options) + public async Task SendMessageAsync(RuleDelay delay, RuleMessagingOptions options) { var mqService = _services.GetService(); if (mqService == null) From 7c6b477c143ff183b6af2a11eef52d149fd36228 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 21 Jan 2026 20:05:21 -0600 Subject: [PATCH 17/36] add custom method action --- .../Agents/Models/AgentRule.cs | 6 +++--- .../Rules/Enums/RuleActionType.cs | 2 ++ .../BotSharp.Abstraction/Rules/IRuleAction.cs | 5 ++++- .../Rules/Options/RuleActionOptions.cs | 9 +++++++-- ...gOptions.cs => RuleEventMessageOptions.cs} | 2 +- .../Rules/Options/RuleMethodOptions.cs | 6 ++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 19 ++++++++++++------- ...ction.Messaging.cs => RuleAction.Event.cs} | 8 ++++---- .../Services/RuleAction.Method.cs | 18 ++++++++++++++++++ .../Services/RabbitMQService.cs | 7 +++++-- 10 files changed, 62 insertions(+), 20 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Rules/Options/{RuleMessagingOptions.cs => RuleEventMessageOptions.cs} (94%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs rename src/Infrastructure/BotSharp.Core.Rules/Services/{RuleAction.Messaging.cs => RuleAction.Event.cs} (84%) create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index d8cd66137..e156cfda8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -13,11 +13,11 @@ public class AgentRule [JsonPropertyName("criteria")] public string Criteria { get; set; } = string.Empty; - [JsonPropertyName("delay")] - public RuleDelay? Delay { get; set; } - [JsonPropertyName("action")] public string? Action { get; set; } + + [JsonPropertyName("delay")] + public RuleDelay? Delay { get; set; } } public class RuleDelay diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs index f7bb0a383..acb05e417 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs @@ -4,4 +4,6 @@ public static class RuleActionType { public const string Chat = "chat"; public const string Http = "http"; + public const string EventMessage = "event-message"; + public const string Method = "method"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 1ee05cf13..70306a1a5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -12,6 +12,9 @@ Task SendChatAsync(Agent agent, RuleChatActionPayload payload) Task SendHttpRequestAsync() => throw new NotImplementedException(); - Task SendMessageAsync(RuleDelay delay, RuleMessagingOptions options) + Task SendEventMessageAsync(RuleDelay delay, RuleEventMessageOptions? options) + => throw new NotImplementedException(); + + Task ExecuteMethodAsync(Agent agent, Func func) => throw new NotImplementedException(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs index 919ed25ef..a17bb2584 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs @@ -8,7 +8,12 @@ public class RuleActionOptions public string Provider { get; set; } = "botsharp-rule"; /// - /// Delay message options + /// Event message options /// - public RuleMessagingOptions? Messaging { get; set; } + public RuleEventMessageOptions? EventMessage { get; set; } + + /// + /// Custom method options + /// + public RuleMethodOptions? Method { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMessagingOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs similarity index 94% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMessagingOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs index b3a5d91de..5fe228dba 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMessagingOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Rules.Options; -public class RuleMessagingOptions +public class RuleEventMessageOptions { /// /// Message payload diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs new file mode 100644 index 000000000..424efda08 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleMethodOptions +{ + public Func? Func { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index d3512c3a9..69d2d8fa9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -64,16 +64,21 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - if (options?.Action?.Messaging != null) + // Execute action + if (foundTrigger.Action.IsEqualTo(RuleActionType.Method)) { - var isSent = await action.SendMessageAsync(foundTrigger.Delay, options.Action.Messaging); - continue; + if (options?.Action?.Method?.Func != null) + { + await action.ExecuteMethodAsync(agent, options.Action.Method.Func); + } } - - // Execute action - if (foundTrigger.Action.IsEqualTo(RuleActionType.Http)) + else if (foundTrigger.Action.IsEqualTo(RuleActionType.EventMessage)) { - + await action.SendEventMessageAsync(foundTrigger.Delay, options?.Action?.EventMessage); + } + else if (foundTrigger.Action.IsEqualTo(RuleActionType.Http)) + { + } else { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs similarity index 84% rename from src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs index 2c50fb432..50db54fd1 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Messaging.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs @@ -2,15 +2,15 @@ namespace BotSharp.Core.Rules.Services; public partial class RuleAction { - public async Task SendMessageAsync(RuleDelay delay, RuleMessagingOptions options) + public async Task SendEventMessageAsync(RuleDelay delay, RuleEventMessageOptions? options) { - var mqService = _services.GetService(); - if (mqService == null) + if (options == null || delay == null || delay.Quantity < 0) { return false; } - if (delay == null || delay.Quantity < 0) + var mqService = _services.GetService(); + if (mqService == null) { return false; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs new file mode 100644 index 000000000..b036ffc10 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs @@ -0,0 +1,18 @@ +namespace BotSharp.Core.Rules.Services; + +public partial class RuleAction +{ + public async Task ExecuteMethodAsync(Agent agent, Func func) + { + try + { + await func(agent); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when executing custom method."); + return false; + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 872bf2713..27ef23e60 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -259,8 +259,11 @@ public void Dispose() foreach (var item in _consumers) { - item.Value.Consumer?.Dispose(); - item.Value.Channel?.Dispose(); + if (item.Value.Channel != null) + { + item.Value.Channel.Dispose(); + } + item.Value.Consumer.Dispose(); } _disposed = true; From ff57169fc5021183d7bdcd8ad6978cdb548eebb1 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 22 Jan 2026 17:15:31 -0600 Subject: [PATCH 18/36] refine action and add mq channel pool --- .../Agents/Models/AgentRule.cs | 34 ------ .../Rules/Enums/RuleActionType.cs | 9 -- .../Rules/Enums/RuleDelayUnit.cs | 9 -- .../Rules/Hooks/IRuleTriggerHook.cs | 14 +++ .../Rules/Hooks/RuleTriggerHookBase.cs | 5 + .../BotSharp.Abstraction/Rules/IRuleAction.cs | 35 +++--- .../Rules/IRuleCriteria.cs | 4 +- ...tActionPayload.cs => RuleActionContext.cs} | 3 +- .../Rules/Models/RuleActionResult.cs | 46 ++++++++ .../Rules/Models/RuleHttpContext.cs | 13 +++ .../Rules/Models/RuleHttpResult.cs | 6 ++ .../Rules/Options/RuleActionOptions.cs | 19 ---- .../Rules/Options/RuleEventMessageOptions.cs | 34 ------ .../Rules/Options/RuleMethodOptions.cs | 6 -- .../Rules/Options/RuleTriggerOptions.cs | 4 +- .../Constants/RuleHandler.cs | 6 -- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 88 ++++++++------- .../BotSharp.Core.Rules/RulesPlugin.cs | 4 +- .../Services/ChatRuleAction.cs | 68 ++++++++++++ .../Services/RuleAction.Chat.cs | 44 -------- .../Services/RuleAction.Event.cs | 38 ------- .../Services/RuleAction.Http.cs | 9 -- .../Services/RuleAction.Method.cs | 18 ---- .../Services/RuleAction.cs | 17 --- .../Services/RuleCriteria.cs | 8 +- .../BotSharp.Core.Rules/Using.cs | 3 - src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs | 2 +- .../Connections/RabbitMQChannelPool.cs | 73 +++++++++++++ .../Connections/RabbitMQChannelPoolFactory.cs | 13 +++ .../Connections/RabbitMQConnection.cs | 2 + .../Interfaces/IRabbitMQConnection.cs | 1 + .../RabbitMQPlugin.cs | 2 +- .../Services/RabbitMQService.cs | 102 +++++++++++------- src/WebStarter/appsettings.json | 2 +- 34 files changed, 390 insertions(+), 351 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs rename src/Infrastructure/BotSharp.Abstraction/Rules/Models/{RuleChatActionPayload.cs => RuleActionContext.cs} (66%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index e156cfda8..dfe03681d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Rules.Enums; - namespace BotSharp.Abstraction.Agents.Models; public class AgentRule @@ -15,36 +13,4 @@ public class AgentRule [JsonPropertyName("action")] public string? Action { get; set; } - - [JsonPropertyName("delay")] - public RuleDelay? Delay { get; set; } -} - -public class RuleDelay -{ - public int Quantity { get; set; } - public string Unit { get; set; } - - public TimeSpan? Parse() - { - TimeSpan? ts = null; - - switch (Unit) - { - case RuleDelayUnit.Second: - ts = TimeSpan.FromSeconds(Quantity); - break; - case RuleDelayUnit.Minute: - ts = TimeSpan.FromMinutes(Quantity); - break; - case RuleDelayUnit.Hour: - ts = TimeSpan.FromHours(Quantity); - break; - case RuleDelayUnit.Day: - ts = TimeSpan.FromDays(Quantity); - break; - } - - return ts; - } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs deleted file mode 100644 index acb05e417..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Enums; - -public static class RuleActionType -{ - public const string Chat = "chat"; - public const string Http = "http"; - public const string EventMessage = "event-message"; - public const string Method = "method"; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs deleted file mode 100644 index cc862e68b..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Enums; - -public static class RuleDelayUnit -{ - public const string Second = "second"; - public const string Minute = "minute"; - public const string Hour = "hour"; - public const string Day = "day"; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs new file mode 100644 index 000000000..bfeda4086 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -0,0 +1,14 @@ +using BotSharp.Abstraction.Hooks; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Rules.Models; + +namespace BotSharp.Abstraction.Rules.Hooks; + +public interface IRuleTriggerHook : IHookBase +{ + Task BeforeSendEventMessage(Agent agent, RoleDialogModel message) => Task.CompletedTask; + Task AfterSendEventMessage(Agent agent, InstructResult result) => Task.CompletedTask; + + Task BeforeSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpContext message) => Task.CompletedTask; + Task AfterSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpResult result) => Task.CompletedTask; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs new file mode 100644 index 000000000..60bdf7cf5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs @@ -0,0 +1,5 @@ +namespace BotSharp.Abstraction.Rules.Hooks; + +public class RuleTriggerHookBase : IRuleTriggerHook +{ +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 70306a1a5..9c2bf03d9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -1,20 +1,29 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Rules.Models; +using BotSharp.Abstraction.Rules.Options; namespace BotSharp.Abstraction.Rules; +/// +/// Base interface for rule actions that can be executed by the RuleEngine +/// public interface IRuleAction { - string Provider { get; } + /// + /// The unique name of the rule action provider + /// + string Name { get; } - Task SendChatAsync(Agent agent, RuleChatActionPayload payload) - => throw new NotImplementedException(); - - Task SendHttpRequestAsync() - => throw new NotImplementedException(); - - Task SendEventMessageAsync(RuleDelay delay, RuleEventMessageOptions? options) - => throw new NotImplementedException(); - - Task ExecuteMethodAsync(Agent agent, Func func) - => throw new NotImplementedException(); -} + /// + /// Execute the rule action + /// + /// The agent that triggered the rule + /// The rule trigger + /// The action context + /// The action execution result + Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs index 600ebb546..af5d5cf3d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs @@ -4,6 +4,6 @@ public interface IRuleCriteria { string Provider { get; } - Task ExecuteCriteriaAsync(Agent agent, string triggerName, CriteriaExecuteOptions options) - => throw new NotImplementedException(); + Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) + => Task.FromResult(false); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs similarity index 66% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 84c22353a..d6a0d5570 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -1,8 +1,7 @@ namespace BotSharp.Abstraction.Rules.Models; -public class RuleChatActionPayload +public class RuleActionContext { public string Text { get; set; } - public string Channel { get; set; } public IEnumerable? States { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs new file mode 100644 index 000000000..ffdaeab2e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs @@ -0,0 +1,46 @@ +namespace BotSharp.Abstraction.Rules.Models; + +/// +/// Result of a rule action execution +/// +public class RuleActionResult +{ + /// + /// Whether the action executed successfully + /// + public bool Success { get; set; } + + /// + /// The conversation ID if a new conversation was created + /// + public string? ConversationId { get; set; } + + /// + /// Response content from the action + /// + public string? Response { get; set; } + + /// + /// Error message if the action failed + /// + public string? ErrorMessage { get; set; } + + public static RuleActionResult Succeeded(string? response = null) + { + return new RuleActionResult + { + Success = true, + Response = response + }; + } + + public static RuleActionResult Failed(string errorMessage) + { + return new RuleActionResult + { + Success = false, + ErrorMessage = errorMessage + }; + } +} + diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs new file mode 100644 index 000000000..0268d12a1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs @@ -0,0 +1,13 @@ +using System.Net.Http; + +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleHttpContext +{ + public string BaseUrl { get; set; } + public string RelativeUrl { get; set; } + public HttpMethod Method { get; set; } + public Dictionary Headers { get; set; } = []; + public Dictionary QueryParams { get; set; } = []; + public string RequestBody { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs new file mode 100644 index 000000000..c5109a5c7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleHttpResult +{ + public string HttpResponse { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs deleted file mode 100644 index a17bb2584..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleActionOptions -{ - /// - /// Rule action provider - /// - public string Provider { get; set; } = "botsharp-rule"; - - /// - /// Event message options - /// - public RuleEventMessageOptions? EventMessage { get; set; } - - /// - /// Custom method options - /// - public RuleMethodOptions? Method { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs deleted file mode 100644 index 5fe228dba..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleEventMessageOptions -{ - /// - /// Message payload - /// - public string Payload { get; set; } - - /// - /// Exchange - /// - public string Exchange { get; set; } - - /// - /// Routing key - /// - public string RoutingKey { get; set; } - - /// - /// Delayed message id - /// - public string? MessageId { get; set; } - - /// - /// Arguments - /// - public Dictionary Arguments { get; set; } = new(); - - public override string ToString() - { - return $"{Exchange}-{RoutingKey} => {Payload}"; - } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs deleted file mode 100644 index 424efda08..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleMethodOptions -{ - public Func? Func { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 1eb07c86c..93703d98f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -2,7 +2,9 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleTriggerOptions { + /// + /// Criteria options for validating whether the rule should be triggered + /// public RuleCriteriaOptions? Criteria { get; set; } - public RuleActionOptions? Action { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs deleted file mode 100644 index 9c35a4139..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Core.Rules.Constants; - -public static class RuleHandler -{ - public const string DefaultProvider = "botsharp-rule"; -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 69d2d8fa9..6fa72b145 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,5 +1,3 @@ -using System.Data; - namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -33,69 +31,77 @@ public async Task> Triggered(IRuleTrigger trigger, string te var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); foreach (var agent in filteredAgents) { - // Criteria + // Criteria validation if (options?.Criteria != null) { var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? RuleHandler.DefaultProvider)); + .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "botsharp-rule-criteria")); if (criteria == null) { + _logger.LogWarning("No criteria provider found for {Provider}, skipping agent {AgentId}", options.Criteria.Provider, agent.Id); continue; } - var isTriggered = await criteria.ExecuteCriteriaAsync(agent, trigger.Name, options.Criteria); - if (!isTriggered) + var isValid = await criteria.ValidateAsync(agent, trigger, options.Criteria); + if (!isValid) { + _logger.LogDebug("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; } } - var foundTrigger = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundTrigger == null) + var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); + if (foundRule == null) { continue; } - var action = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Action?.Provider ?? RuleHandler.DefaultProvider)); - if (action == null) + var context = new RuleActionContext { - continue; - } - - // Execute action - if (foundTrigger.Action.IsEqualTo(RuleActionType.Method)) + Text = text, + States = states + }; + var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("Chat")!, context); + if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { - if (options?.Action?.Method?.Func != null) - { - await action.ExecuteMethodAsync(agent, options.Action.Method.Func); - } + newConversationIds.Add(result.ConversationId); } - else if (foundTrigger.Action.IsEqualTo(RuleActionType.EventMessage)) - { - await action.SendEventMessageAsync(foundTrigger.Delay, options?.Action?.EventMessage); - } - else if (foundTrigger.Action.IsEqualTo(RuleActionType.Http)) + } + + return newConversationIds; + } + + private async Task ExecuteActionAsync( + Agent agent, + IRuleTrigger trigger, + string actionName, + RuleActionContext context) + { + try + { + // Get all registered rule actions + var actions = _services.GetServices(); + + // Find the matching action + var action = actions.FirstOrDefault(x => x.Name.IsEqualTo(actionName)); + + if (action == null) { - + var errorMsg = $"No rule action {actionName} is found"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); } - else - { - var conversationId = await action.SendChatAsync(agent, payload: new() - { - Text = text, - Channel = trigger.Channel, - States = states - }); - if (!string.IsNullOrEmpty(conversationId)) - { - newConversationIds.Add(conversationId); - } - } - } + _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", + action.Name, agent.Id, trigger.Name); - return newConversationIds; + return await action.ExecuteAsync(agent, trigger, context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", actionName, agent.Id); + return RuleActionResult.Failed(ex.Message); + } } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 6135da50d..aee5a7fc7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -19,6 +19,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) { services.AddScoped(); services.AddScoped(); - services.AddScoped(); + + // Register rule actions + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs new file mode 100644 index 000000000..903bcdb00 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs @@ -0,0 +1,68 @@ +namespace BotSharp.Core.Rules.Services; + +public sealed class ChatRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public ChatRuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Name => "Chat"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + var channel = trigger.Channel; + var convService = _services.GetRequiredService(); + var conv = await convService.NewConversation(new Conversation + { + Channel = channel, + Title = context.Text, + AgentId = agent.Id + }); + + var message = new RoleDialogModel(AgentRole.User, context.Text); + + var allStates = new List + { + new("channel", channel) + }; + + if (!context.States.IsNullOrEmpty()) + { + allStates.AddRange(context.States!); + } + + await convService.SetConversationId(conv.Id, allStates); + await convService.SendMessage(agent.Id, + message, + null, + msg => Task.CompletedTask); + + await convService.SaveStates(); + + _logger.LogInformation("Chat rule action executed successfully for agent {AgentId}, conversation {ConversationId}", agent.Id, conv.Id); + + return new RuleActionResult + { + Success = true, + ConversationId = conv.Id + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error when sending chat via rule action for agent {AgentId} and trigger {TriggerName}", agent.Id, trigger.Name); + return RuleActionResult.Failed(ex.Message); + } + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs deleted file mode 100644 index 84925a92d..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction : IRuleAction -{ - public async Task SendChatAsync(Agent agent, RuleChatActionPayload payload) - { - try - { - var convService = _services.GetRequiredService(); - var conv = await convService.NewConversation(new Conversation - { - Channel = payload.Channel, - Title = payload.Text, - AgentId = agent.Id - }); - - var message = new RoleDialogModel(AgentRole.User, payload.Text); - - var allStates = new List - { - new("channel", payload.Channel) - }; - - if (!payload.States.IsNullOrEmpty()) - { - allStates.AddRange(payload.States!); - } - - await convService.SetConversationId(conv.Id, allStates); - await convService.SendMessage(agent.Id, - message, - null, - msg => Task.CompletedTask); - - await convService.SaveStates(); - return conv.Id; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when sending chat via rule action."); - return string.Empty; - } - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs deleted file mode 100644 index 50db54fd1..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction -{ - public async Task SendEventMessageAsync(RuleDelay delay, RuleEventMessageOptions? options) - { - if (options == null || delay == null || delay.Quantity < 0) - { - return false; - } - - var mqService = _services.GetService(); - if (mqService == null) - { - return false; - } - - var ts = delay.Parse(); - if (!ts.HasValue) - { - return false; - } - - _logger.LogWarning($"Start sending delay message {options}"); - - var isSent = await mqService.PublishAsync(options.Payload, options: new() - { - Exchange = options.Exchange, - RoutingKey = options.RoutingKey, - MessageId = options.MessageId, - MilliSeconds = (long)ts.Value.TotalMilliseconds, - Arguments = options.Arguments - }); - - _logger.LogWarning($"Complete sending delay message: {(isSent ? "Success" : "Failed")}"); - return isSent; - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs deleted file mode 100644 index 01590a3be..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction -{ - public Task SendHttpRequestAsync() - { - throw new NotImplementedException(); - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs deleted file mode 100644 index b036ffc10..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction -{ - public async Task ExecuteMethodAsync(Agent agent, Func func) - { - try - { - await func(agent); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when executing custom method."); - return false; - } - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs deleted file mode 100644 index 0fd271550..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction : IRuleAction -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - - public RuleAction( - IServiceProvider services, - ILogger logger) - { - _services = services; - _logger = logger; - } - - public string Provider => RuleHandler.DefaultProvider; -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs index 2ae06253d..015c6e910 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs @@ -18,9 +18,9 @@ public RuleCriteria( _codingSettings = codingSettings; } - public string Provider => RuleHandler.DefaultProvider; + public string Provider => "botsharp-rule-criteria"; - public async Task ExecuteCriteriaAsync(Agent agent, string triggerName, CriteriaExecuteOptions options) + public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) { if (string.IsNullOrWhiteSpace(agent?.Id)) { @@ -36,10 +36,10 @@ public async Task ExecuteCriteriaAsync(Agent agent, string triggerName, Cr } var agentService = _services.GetRequiredService(); - var scriptName = options.CodeScriptName ?? $"{triggerName}_rule.py"; + var scriptName = options.CodeScriptName ?? $"{trigger.Name}_rule.py"; var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - var msg = $"rule trigger ({triggerName}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; + var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index 0f999ff88..2cfb617d2 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -16,7 +16,6 @@ global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Repositories.Filters; global using BotSharp.Abstraction.Rules; -global using BotSharp.Abstraction.Rules.Enums; global using BotSharp.Abstraction.Rules.Options; global using BotSharp.Abstraction.Rules.Models; global using BotSharp.Abstraction.Utilities; @@ -27,5 +26,3 @@ global using BotSharp.Abstraction.Coding.Utils; global using BotSharp.Abstraction.Coding.Settings; global using BotSharp.Abstraction.Hooks; - -global using BotSharp.Core.Rules.Constants; diff --git a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs index be189898e..8e29bb1bb 100644 --- a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs +++ b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs @@ -84,7 +84,7 @@ private async Task SendRequest(string url, GraphQueryRequest r } catch (Exception ex) { - _logger.LogError(ex, $"Error when fetching Lessen GLM response (Endpoint: {url})."); + _logger.LogError(ex, $"Error when fetching {Provider} Graph db response (Endpoint: {url})."); return result; } } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs new file mode 100644 index 000000000..81c7de270 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.ObjectPool; +using RabbitMQ.Client; + +namespace BotSharp.Plugin.RabbitMQ.Connections; + +public class RabbitMQChannelPool +{ + private readonly ObjectPool _pool; + private readonly ILogger _logger; + private readonly int _tryLimit = 3; + + public RabbitMQChannelPool( + IServiceProvider services, + IRabbitMQConnection mqConnection) + { + _logger = services.GetRequiredService().CreateLogger(); + var poolProvider = new DefaultObjectPoolProvider(); + var policy = new ChannelPoolPolicy(mqConnection.Connection); + _pool = poolProvider.Create(policy); + } + + public IChannel Get() + { + var count = 0; + var channel = _pool.Get(); + + while (count < _tryLimit && channel.IsClosed) + { + channel.Dispose(); + channel = _pool.Get(); + count++; + } + + if (channel.IsClosed) + { + _logger.LogWarning($"No open channel from the pool after {_tryLimit} retries."); + } + + return channel; + } + + public void Return(IChannel channel) + { + if (channel.IsOpen) + { + _pool.Return(channel); + } + else + { + channel.Dispose(); + } + } +} + +internal class ChannelPoolPolicy : IPooledObjectPolicy +{ + private readonly IConnection _connection; + + public ChannelPoolPolicy(IConnection connection) + { + _connection = connection; + } + + public IChannel Create() + { + return _connection.CreateChannelAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public bool Return(IChannel obj) + { + return true; + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs new file mode 100644 index 000000000..989c0a7b7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; + +namespace BotSharp.Plugin.RabbitMQ.Connections; + +public static class RabbitMQChannelPoolFactory +{ + private static readonly ConcurrentDictionary _poolDict = new(); + + public static RabbitMQChannelPool GetChannelPool(IServiceProvider services, IRabbitMQConnection rabbitMQConnection) + { + return _poolDict.GetOrAdd(rabbitMQConnection.Connection.ToString()!, key => new RabbitMQChannelPool(services, rabbitMQConnection)); + } +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs index c2782e9fe..dac9e8c07 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs @@ -38,6 +38,8 @@ public RabbitMQConnection( public bool IsConnected => _connection != null && _connection.IsOpen && !_disposed; + public IConnection Connection => _connection; + public async Task CreateChannelAsync() { if (!IsConnected) diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs index c5266d630..cb89c2976 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs @@ -5,6 +5,7 @@ namespace BotSharp.Plugin.RabbitMQ.Interfaces; public interface IRabbitMQConnection : IDisposable { bool IsConnected { get; } + IConnection Connection { get; } Task CreateChannelAsync(); Task ConnectAsync(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index 3a841fcd1..d1ddc75c3 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -16,7 +16,7 @@ public class RabbitMQPlugin : IBotSharpAppPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { var settings = new RabbitMQSettings(); - config.Bind("RabbitMQ", settings); + config.Bind("RabbitMessageQueue", settings); services.AddSingleton(settings); var mqSettings = new MessageQueueSettings(); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 27ef23e60..529660abe 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -1,3 +1,4 @@ +using BotSharp.Plugin.RabbitMQ.Connections; using Polly; using Polly.Retry; using RabbitMQ.Client; @@ -9,6 +10,7 @@ namespace BotSharp.Plugin.RabbitMQ.Services; public class RabbitMQService : IMQService { private readonly IRabbitMQConnection _mqConnection; + private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly int _retryCount = 5; @@ -17,9 +19,11 @@ public class RabbitMQService : IMQService public RabbitMQService( IRabbitMQConnection mqConnection, + IServiceProvider services, ILogger logger) { _mqConnection = mqConnection; + _services = services; _logger = logger; } @@ -150,11 +154,17 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel data = Encoding.UTF8.GetString(eventArgs.Body.Span); _logger.LogInformation($"Message received on '{options.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); - await registration.Consumer.HandleMessageAsync(options.QueueName, data); - + var isDone = await registration.Consumer.HandleMessageAsync(options.QueueName, data); if (!options.AutoAck && registration.Channel != null) { - await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); + if (isDone) + { + await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); + } + else + { + await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); + } } } catch (Exception ex) @@ -176,52 +186,68 @@ public async Task PublishAsync(T payload, MQPublishOptions options) await _mqConnection.ConnectAsync(); } + var isPublished = false; var policy = BuildRetryPolicy(); await policy.Execute(async () => { - await using var channel = await _mqConnection.CreateChannelAsync(); - var args = new Dictionary - { - ["x-delayed-type"] = "direct" - }; + var channelPool = RabbitMQChannelPoolFactory.GetChannelPool(_services, _mqConnection); + var channel = channelPool.Get(); - if (options.Arguments != null) + try { - foreach (var kvp in options.Arguments) + var args = new Dictionary { - args[kvp.Key] = kvp.Value; - } - } + ["x-delayed-type"] = "direct" + }; - await channel.ExchangeDeclareAsync( - exchange: options.Exchange, - type: "x-delayed-message", - durable: true, - autoDelete: false, - arguments: args); - - var messageId = options.MessageId ?? Guid.NewGuid().ToString(); - var message = new MQMessage(payload, messageId); - var body = ConvertToBinary(message); - var properties = new BasicProperties - { - MessageId = messageId, - DeliveryMode = DeliveryModes.Persistent, - Headers = new Dictionary + if (options.Arguments != null) { - ["x-delay"] = options.MilliSeconds + foreach (var kvp in options.Arguments) + { + args[kvp.Key] = kvp.Value; + } } - }; - - await channel.BasicPublishAsync( - exchange: options.Exchange, - routingKey: options.RoutingKey, - mandatory: true, - basicProperties: properties, - body: body); + + await channel.ExchangeDeclareAsync( + exchange: options.Exchange, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); + + var messageId = options.MessageId ?? Guid.NewGuid().ToString(); + var message = new MQMessage(payload, messageId); + var body = ConvertToBinary(message); + var properties = new BasicProperties + { + MessageId = messageId, + DeliveryMode = DeliveryModes.Persistent, + Headers = new Dictionary + { + ["x-delay"] = options.MilliSeconds + } + }; + + await channel.BasicPublishAsync( + exchange: options.Exchange, + routingKey: options.RoutingKey, + mandatory: true, + basicProperties: properties, + body: body); + + isPublished = true; + } + catch (Exception) + { + throw; + } + finally + { + channelPool.Return(channel); + } }); - return true; + return isPublished; } catch (Exception ex) { diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 322315192..62e55d45f 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1025,7 +1025,7 @@ "Provider": "RabbitMQ" }, - "RabbitMQ": { + "RabbitMessageQueue": { "HostName": "localhost", "Port": 5672, "UserName": "guest", From ee2e444193cdad250333d7349dd9ef7a94518d73 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 22 Jan 2026 20:09:18 -0600 Subject: [PATCH 19/36] refine config --- .../MessageQueues/IMQConsumer.cs | 4 +- .../MessageQueues/MQConsumerBase.cs | 4 +- ...ConsumerOptions.cs => MQConsumerConfig.cs} | 2 +- .../MessageQueues/Models/MQPublishOptions.cs | 29 +++++++++-- .../Consumers/DummyMessageConsumer.cs | 2 +- .../Consumers/ScheduledMessageConsumer.cs | 2 +- .../Controllers/RabbitMQController.cs | 4 +- .../Models/RabbitMQConsumerConfig.cs | 29 +++++++++++ .../RabbitMQPlugin.cs | 2 - .../Services/RabbitMQService.cs | 48 +++++++++---------- src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs | 3 +- 11 files changed, 90 insertions(+), 39 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/{MQConsumerOptions.cs => MQConsumerConfig.cs} (97%) create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs index f0aff8c1b..25fcda5a2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -9,9 +9,9 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues; public interface IMQConsumer : IDisposable { /// - /// Gets the consumer options containing exchange, queue and routing configuration. + /// Gets the consumer config /// - MQConsumerOptions Options { get; } + object Config { get; } /// /// Handles the received message from the queue. diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index e38c26d9b..73b7572bc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -16,10 +16,10 @@ public abstract class MQConsumerBase : IMQConsumer private bool _disposed = false; /// - /// Gets the consumer options for this consumer. + /// Gets the consumer config for this consumer. /// Override this property to customize exchange, queue and routing configuration. /// - public abstract MQConsumerOptions Options { get; } + public abstract object Config { get; } protected MQConsumerBase( IServiceProvider services, diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs similarity index 97% rename from src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs index 7aa9bf02e..bb3072a14 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs @@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; /// Configuration options for message queue consumers. /// These options are MQ-product agnostic and can be adapted by different implementations. /// -public class MQConsumerOptions +public class MQConsumerConfig { /// /// The exchange name (topic in some MQ systems). diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs index a71df3574..b7c31d20e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs @@ -1,10 +1,33 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; +/// +/// Configuration options for publishing messages to a message queue. +/// These options are MQ-product agnostic and can be adapted by different implementations. +/// public class MQPublishOptions { - public string Exchange { get; set; } - public string RoutingKey { get; set; } - public long MilliSeconds { get; set; } + /// + /// The topic name (exchange in RabbitMQ, topic in Kafka/Azure Service Bus). + /// + public string TopicName { get; set; } = string.Empty; + + /// + /// The routing key (partition key in some MQ systems, used for message routing). + /// + public string RoutingKey { get; set; } = string.Empty; + + /// + /// Delay in milliseconds before the message is delivered. + /// + public long DelayMilliseconds { get; set; } + + /// + /// Optional unique identifier for the message. + /// public string? MessageId { get; set; } + + /// + /// Additional arguments for the publish configuration (MQ-specific). + /// public Dictionary Arguments { get; set; } = new(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs index 4ce9282cb..bdefa3131 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class DummyMessageConsumer : MQConsumerBase { - public override MQConsumerOptions Options => new() + public override MQConsumerConfig Config => new() { ExchangeName = "my.exchange", QueueName = "dummy.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs index b2deb177e..b28089a94 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class ScheduledMessageConsumer : MQConsumerBase { - public override MQConsumerOptions Options => new() + public override object Config => new { ExchangeName = "my.exchange", QueueName = "scheduled.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs index be9a3d834..802e4fa1b 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs @@ -45,9 +45,9 @@ public async Task PublishScheduledMessage([FromBody] PublishSched payload, options: new() { - Exchange = "my.exchange", + TopicName = "my.exchange", RoutingKey = "my.routing", - MilliSeconds = request.DelayMilliseconds ?? 10000, + DelayMilliseconds = request.DelayMilliseconds ?? 10000, MessageId = request.MessageId }); return Ok(new { Success = success }); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs new file mode 100644 index 000000000..1790fe1ab --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs @@ -0,0 +1,29 @@ +namespace BotSharp.Plugin.RabbitMQ.Models; + +internal class RabbitMQConsumerConfig +{ + /// + /// The exchange name (topic in some MQ systems). + /// + public string ExchangeName { get; set; } = "rabbitmq.exchange"; + + /// + /// The queue name (subscription in some MQ systems). + /// + public string QueueName { get; set; } = "rabbitmq.queue"; + + /// + /// The routing key (filter in some MQ systems). + /// + public string RoutingKey { get; set; } = "rabbitmq.routing"; + + /// + /// Whether to automatically acknowledge messages. + /// + public bool AutoAck { get; set; } = false; + + /// + /// Additional arguments for the consumer configuration. + /// + public Dictionary Arguments { get; set; } = new(); +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index d1ddc75c3..9da987eec 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -1,8 +1,6 @@ -using BotSharp.Plugin.RabbitMQ.Connections; using BotSharp.Plugin.RabbitMQ.Services; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace BotSharp.Plugin.RabbitMQ; diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 529660abe..96c53f1ad 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -1,4 +1,3 @@ -using BotSharp.Plugin.RabbitMQ.Connections; using Polly; using Polly.Retry; using RabbitMQ.Client; @@ -38,7 +37,8 @@ public async Task SubscribeAsync(string key, IMQConsumer consumer) var registration = await CreateConsumerRegistrationAsync(consumer); if (registration != null && _consumers.TryAdd(key, registration)) { - _logger.LogInformation($"Consumer '{key}' subscribed to queue '{consumer.Options.QueueName}'."); + var config = consumer.Config as RabbitMQConsumerConfig ?? new(); + _logger.LogInformation($"Consumer '{key}' subscribed to queue '{config.QueueName}'."); return true; } @@ -75,7 +75,7 @@ public async Task UnsubscribeAsync(string key) { var channel = await CreateChannelAsync(consumer); - var options = consumer.Options; + var config = consumer.Config as RabbitMQConsumerConfig ?? new(); var registration = new ConsumerRegistration(consumer, channel); var asyncConsumer = new AsyncEventingBasicConsumer(channel); @@ -85,11 +85,11 @@ public async Task UnsubscribeAsync(string key) }; await channel.BasicConsumeAsync( - queue: options.QueueName, - autoAck: options.AutoAck, + queue: config.QueueName, + autoAck: config.AutoAck, consumer: asyncConsumer); - _logger.LogWarning($"RabbitMQ consuming queue '{options.QueueName}'."); + _logger.LogWarning($"RabbitMQ consuming queue '{config.QueueName}'."); return registration; } catch (Exception ex) @@ -106,40 +106,40 @@ private async Task CreateChannelAsync(IMQConsumer consumer) await _mqConnection.ConnectAsync(); } - var options = consumer.Options; + var config = consumer.Config as RabbitMQConsumerConfig ?? new(); var channel = await _mqConnection.CreateChannelAsync(); - _logger.LogWarning($"Created RabbitMQ channel {channel.ChannelNumber} for queue '{options.QueueName}'"); + _logger.LogWarning($"Created RabbitMQ channel {channel.ChannelNumber} for queue '{config.QueueName}'"); var args = new Dictionary { ["x-delayed-type"] = "direct" }; - if (options.Arguments != null) + if (config.Arguments != null) { - foreach (var kvp in options.Arguments) + foreach (var kvp in config.Arguments) { args[kvp.Key] = kvp.Value; } } await channel.ExchangeDeclareAsync( - exchange: options.ExchangeName, + exchange: config.ExchangeName, type: "x-delayed-message", durable: true, autoDelete: false, arguments: args); await channel.QueueDeclareAsync( - queue: options.QueueName, + queue: config.QueueName, durable: true, exclusive: false, autoDelete: false); await channel.QueueBindAsync( - queue: options.QueueName, - exchange: options.ExchangeName, - routingKey: options.RoutingKey); + queue: config.QueueName, + exchange: config.ExchangeName, + routingKey: config.RoutingKey); return channel; } @@ -147,15 +147,15 @@ await channel.QueueBindAsync( private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDeliverEventArgs eventArgs) { var data = string.Empty; - var options = registration.Consumer.Options; + var config = registration.Consumer.Config as RabbitMQConsumerConfig ?? new(); try { data = Encoding.UTF8.GetString(eventArgs.Body.Span); - _logger.LogInformation($"Message received on '{options.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); + _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); - var isDone = await registration.Consumer.HandleMessageAsync(options.QueueName, data); - if (!options.AutoAck && registration.Channel != null) + var isDone = await registration.Consumer.HandleMessageAsync(config.QueueName, data); + if (!config.AutoAck && registration.Channel != null) { if (isDone) { @@ -169,8 +169,8 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel } catch (Exception ex) { - _logger.LogError(ex, $"Error consuming message on queue '{options.QueueName}': {data}"); - if (!options.AutoAck && registration.Channel != null) + _logger.LogError(ex, $"Error consuming message on queue '{config.QueueName}': {data}"); + if (!config.AutoAck && registration.Channel != null) { await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); } @@ -209,7 +209,7 @@ await policy.Execute(async () => } await channel.ExchangeDeclareAsync( - exchange: options.Exchange, + exchange: options.TopicName, type: "x-delayed-message", durable: true, autoDelete: false, @@ -224,12 +224,12 @@ await channel.ExchangeDeclareAsync( DeliveryMode = DeliveryModes.Persistent, Headers = new Dictionary { - ["x-delay"] = options.MilliSeconds + ["x-delay"] = options.DelayMilliseconds } }; await channel.BasicPublishAsync( - exchange: options.Exchange, + exchange: options.TopicName, routingKey: options.RoutingKey, mandatory: true, basicProperties: properties, diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs index 508d3a4ea..0a8a8c3a5 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs @@ -34,4 +34,5 @@ global using BotSharp.Plugin.RabbitMQ.Settings; global using BotSharp.Plugin.RabbitMQ.Models; global using BotSharp.Plugin.RabbitMQ.Interfaces; -global using BotSharp.Plugin.RabbitMQ.Consumers; \ No newline at end of file +global using BotSharp.Plugin.RabbitMQ.Consumers; +global using BotSharp.Plugin.RabbitMQ.Connections; \ No newline at end of file From 436990d04c55fb7c13f21c8fbf45cbf2fd4b5711 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 22 Jan 2026 21:58:51 -0600 Subject: [PATCH 20/36] fix mq config --- .../MessageQueues/IMQConsumer.cs | 1 - .../MessageQueues/MQConsumerBase.cs | 1 - .../MessageQueues/Models/MQConsumerConfig.cs | 34 ------------------- .../Consumers/DummyMessageConsumer.cs | 2 +- .../Consumers/ScheduledMessageConsumer.cs | 2 +- .../Models/RabbitMQConsumerConfig.cs | 10 +++--- .../RabbitMQPlugin.cs | 2 +- .../Services/RabbitMQService.cs | 11 ++++-- src/WebStarter/appsettings.json | 2 +- 9 files changed, 17 insertions(+), 48 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs index 25fcda5a2..2f9296689 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -21,4 +21,3 @@ public interface IMQConsumer : IDisposable /// True if the message was handled successfully, false otherwise Task HandleMessageAsync(string channel, string data); } - diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index 73b7572bc..2fda58581 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Infrastructures.MessageQueues; -using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; using Microsoft.Extensions.Logging; namespace BotSharp.Plugin.RabbitMQ.Consumers; diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs deleted file mode 100644 index bb3072a14..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; - -/// -/// Configuration options for message queue consumers. -/// These options are MQ-product agnostic and can be adapted by different implementations. -/// -public class MQConsumerConfig -{ - /// - /// The exchange name (topic in some MQ systems). - /// - public string ExchangeName { get; set; } = string.Empty; - - /// - /// The queue name (subscription in some MQ systems). - /// - public string QueueName { get; set; } = string.Empty; - - /// - /// The routing key (filter in some MQ systems). - /// - public string RoutingKey { get; set; } = string.Empty; - - /// - /// Whether to automatically acknowledge messages. - /// - public bool AutoAck { get; set; } = false; - - /// - /// Additional arguments for the consumer configuration. - /// - public Dictionary Arguments { get; set; } = new(); -} - diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs index bdefa3131..36af0df90 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class DummyMessageConsumer : MQConsumerBase { - public override MQConsumerConfig Config => new() + public override object Config => new RabbitMQConsumerConfig { ExchangeName = "my.exchange", QueueName = "dummy.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs index b28089a94..f6040dcd7 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class ScheduledMessageConsumer : MQConsumerBase { - public override object Config => new + public override object Config => new RabbitMQConsumerConfig { ExchangeName = "my.exchange", QueueName = "scheduled.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs index 1790fe1ab..4dc8f8ed5 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs @@ -5,25 +5,25 @@ internal class RabbitMQConsumerConfig /// /// The exchange name (topic in some MQ systems). /// - public string ExchangeName { get; set; } = "rabbitmq.exchange"; + internal string ExchangeName { get; set; } = "rabbitmq.exchange"; /// /// The queue name (subscription in some MQ systems). /// - public string QueueName { get; set; } = "rabbitmq.queue"; + internal string QueueName { get; set; } = "rabbitmq.queue"; /// /// The routing key (filter in some MQ systems). /// - public string RoutingKey { get; set; } = "rabbitmq.routing"; + internal string RoutingKey { get; set; } = "rabbitmq.routing"; /// /// Whether to automatically acknowledge messages. /// - public bool AutoAck { get; set; } = false; + internal bool AutoAck { get; set; } = false; /// /// Additional arguments for the consumer configuration. /// - public Dictionary Arguments { get; set; } = new(); + internal Dictionary Arguments { get; set; } = new(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index 9da987eec..ff45dfe48 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -14,7 +14,7 @@ public class RabbitMQPlugin : IBotSharpAppPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { var settings = new RabbitMQSettings(); - config.Bind("RabbitMessageQueue", settings); + config.Bind("RabbitMQ", settings); services.AddSingleton(settings); var mqSettings = new MessageQueueSettings(); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 96c53f1ad..0ea2465a7 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -154,10 +154,10 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel data = Encoding.UTF8.GetString(eventArgs.Body.Span); _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); - var isDone = await registration.Consumer.HandleMessageAsync(config.QueueName, data); + var isHandled = await registration.Consumer.HandleMessageAsync(config.QueueName, data); if (!config.AutoAck && registration.Channel != null) { - if (isDone) + if (isHandled) { await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); } @@ -181,6 +181,11 @@ public async Task PublishAsync(T payload, MQPublishOptions options) { try { + if (options == null) + { + return false; + } + if (!_mqConnection.IsConnected) { await _mqConnection.ConnectAsync(); @@ -200,7 +205,7 @@ await policy.Execute(async () => ["x-delayed-type"] = "direct" }; - if (options.Arguments != null) + if (!options.Arguments.IsNullOrEmpty()) { foreach (var kvp in options.Arguments) { diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 62e55d45f..322315192 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1025,7 +1025,7 @@ "Provider": "RabbitMQ" }, - "RabbitMessageQueue": { + "RabbitMQ": { "HostName": "localhost", "Port": 5672, "UserName": "guest", From 528147936830dee0719fd59f79a8978ff8fff128 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Sat, 24 Jan 2026 18:31:11 -0600 Subject: [PATCH 21/36] minor change --- .../Repository/MongoRepository.Conversation.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 300700a64..83b2f1658 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -634,8 +634,7 @@ public async Task> TruncateConversation(string conversationId, stri continue; } - var values = state.Values.Where(x => x.MessageId != messageId) - .Where(x => x.UpdateTime < refTime) + var values = state.Values.Where(x => x.MessageId != messageId && x.UpdateTime < refTime) .ToList(); if (values.Count == 0) continue; From 77c2adb1092385d2af5264132ecabc68822afb28 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 14:27:19 -0600 Subject: [PATCH 22/36] refien rule action --- .../Agents/Models/AgentRule.cs | 14 ++ .../Rules/Hooks/IRuleTriggerHook.cs | 8 +- .../Rules/Hooks/RuleTriggerHookBase.cs | 13 ++ .../Rules/Models/RuleActionContext.cs | 4 +- .../Rules/Models/RuleHttpContext.cs | 13 -- .../Rules/Models/RuleHttpResult.cs | 6 - .../Rules/Options/RuleTriggerOptions.cs | 1 - .../Utilities/ObjectExtensions.cs | 38 ++++ .../Controllers/RuleController.cs | 42 ++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 81 +++++++- .../Models/RuleMessagePayload.cs | 11 + .../Models/RuleTriggerActionRequest.cs | 18 ++ .../BotSharp.Core.Rules/RulesPlugin.cs | 3 + .../Services/ChatRuleAction.cs | 10 +- .../Services/FunctionCallRuleAction.cs | 47 +++++ .../Services/HttpRuleAction.cs | 191 ++++++++++++++++++ .../Services/MessageQueueRuleAction.cs | 125 ++++++++++++ .../BotSharp.Core.Rules/Using.cs | 2 + .../Controllers/Agent/AgentController.Rule.cs | 7 +- .../Models/AgentRuleMongoElement.cs | 11 +- 20 files changed, 605 insertions(+), 40 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index dfe03681d..6e31e1b71 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Agents.Models; public class AgentRule @@ -13,4 +15,16 @@ public class AgentRule [JsonPropertyName("action")] public string? Action { get; set; } + + /// + /// Adaptive configuration for rule actions. + /// This flexible JSON document can store any action-specific configuration. + /// The structure depends on the action type: + /// - For "Http" action: contains http_context with base_url, relative_url, method, etc. + /// - For "MessageQueue" action: contains mq_config with topic_name, routing_key, etc. + /// - For custom actions: can contain any custom configuration structure + /// + [JsonPropertyName("action_config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonDocument? ActionConfig { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index bfeda4086..08195c89c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -1,14 +1,10 @@ using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Rules.Models; namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { - Task BeforeSendEventMessage(Agent agent, RoleDialogModel message) => Task.CompletedTask; - Task AfterSendEventMessage(Agent agent, InstructResult result) => Task.CompletedTask; - - Task BeforeSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpContext message) => Task.CompletedTask; - Task AfterSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs index 60bdf7cf5..f3b99cbe8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs @@ -1,5 +1,18 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules.Hooks; public class RuleTriggerHookBase : IRuleTriggerHook { + public string SelfId = string.Empty; + + public Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) + { + return Task.CompletedTask; + } + + public Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) + { + return Task.CompletedTask; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index d6a0d5570..12a0a3e66 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -2,6 +2,6 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { - public string Text { get; set; } - public IEnumerable? States { get; set; } + public string Text { get; set; } = string.Empty; + public Dictionary States { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs deleted file mode 100644 index 0268d12a1..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net.Http; - -namespace BotSharp.Abstraction.Rules.Models; - -public class RuleHttpContext -{ - public string BaseUrl { get; set; } - public string RelativeUrl { get; set; } - public HttpMethod Method { get; set; } - public Dictionary Headers { get; set; } = []; - public Dictionary QueryParams { get; set; } = []; - public string RequestBody { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs deleted file mode 100644 index c5109a5c7..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Models; - -public class RuleHttpResult -{ - public string HttpResponse { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 93703d98f..70567ea6d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -7,4 +7,3 @@ public class RuleTriggerOptions /// public RuleCriteriaOptions? Criteria { get; set; } } - diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs index a36516c8a..9efdf6f42 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs @@ -65,4 +65,42 @@ public static class ObjectExtensions return null; } } + + public static T? TryGetValueOrDefault(this IDictionary dict, string key, T? defaultValue = default, JsonSerializerOptions? jsonOptions = null) + { + return dict.TryGetValue(key, out var value, jsonOptions) + ? value! + : defaultValue; + } + + public static bool TryGetValue(this IDictionary dict, string key, out T? result, JsonSerializerOptions? jsonOptions = null) + { + result = default; + + if (!dict.TryGetValue(key, out var value) || value is null) + { + return false; + } + + if (value is T t) + { + result = t; + return true; + } + + if (value is JsonElement je) + { + try + { + result = je.Deserialize(jsonOptions); + return true; + } + catch + { + return false; + } + } + + return false; + } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs new file mode 100644 index 000000000..ebb2bf0c5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs @@ -0,0 +1,42 @@ +using BotSharp.Core.Rules.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BotSharp.Core.Rules.Controllers; + +[Authorize] +[ApiController] +public class RuleController : ControllerBase +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IRuleEngine _ruleEngine; + + public RuleController( + IServiceProvider services, + ILogger logger, + IRuleEngine ruleEngine) + { + _services = services; + _logger = logger; + _ruleEngine = ruleEngine; + } + + [HttpPost("/rule/trigger/run")] + public async Task RunAction([FromBody] RuleTriggerActionRequest request) + { + if (request == null) + { + return BadRequest(new { Success = false, Error = "Request cannnot be empty." }); + } + + var trigger = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(request.TriggerName)); + if (trigger == null) + { + return BadRequest(new { Success = false, Error = "Unable to find rule trigger." }); + } + + var result = await _ruleEngine.Triggered(trigger, request.Text, request.States, request.Options); + return Ok(new { Success = true }); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6fa72b145..a485e124e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,3 +1,6 @@ +using BotSharp.Abstraction.Rules.Hooks; +using System.Text.Json; + namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -60,9 +63,9 @@ public async Task> Triggered(IRuleTrigger trigger, string te var context = new RuleActionContext { Text = text, - States = states + States = BuildRuleActionContext(foundRule, states) }; - var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("Chat")!, context); + var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("BotSharp-chat")!, context); if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { newConversationIds.Add(result.ConversationId); @@ -72,6 +75,26 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } + private Dictionary BuildRuleActionContext(AgentRule rule, IEnumerable? states) + { + var dict = new Dictionary(); + + if (rule.ActionConfig != null) + { + dict = ConvertToDictionary(rule.ActionConfig); + } + + if (!states.IsNullOrEmpty()) + { + foreach (var state in states!) + { + dict[state.Key] = state.Value; + } + } + + return dict; + } + private async Task ExecuteActionAsync( Agent agent, IRuleTrigger trigger, @@ -96,7 +119,24 @@ private async Task ExecuteActionAsync( _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", action.Name, agent.Id, trigger.Name); - return await action.ExecuteAsync(agent, trigger, context); + // Combine states + + + var hooks = _services.GetHooks(agent.Id); + foreach (var hook in hooks) + { + await hook.BeforeRuleActionExecuted(agent, trigger, context); + } + + // Execute action + var result = await action.ExecuteAsync(agent, trigger, context); + + foreach (var hook in hooks) + { + await hook.AfterRuleActionExecuted(agent, trigger, result); + } + + return result; } catch (Exception ex) { @@ -104,4 +144,39 @@ private async Task ExecuteActionAsync( return RuleActionResult.Failed(ex.Message); } } + + public static Dictionary ConvertToDictionary(JsonDocument doc) + { + var dict = new Dictionary(); + + foreach (var prop in doc.RootElement.EnumerateObject()) + { + dict[prop.Name] = prop.Value.ValueKind switch + { + JsonValueKind.String => prop.Value.GetString(), + JsonValueKind.Number when prop.Value.TryGetInt32(out int intValue) => intValue, + JsonValueKind.Number when prop.Value.TryGetInt64(out long longValue) => longValue, + JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, + JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, + JsonValueKind.Number when prop.Value.TryGetByte(out byte byteValue) => byteValue, + JsonValueKind.Number when prop.Value.TryGetSByte(out sbyte sbyteValue) => sbyteValue, + JsonValueKind.Number when prop.Value.TryGetUInt16(out ushort uint16Value) => uint16Value, + JsonValueKind.Number when prop.Value.TryGetUInt32(out uint uint32Value) => uint32Value, + JsonValueKind.Number when prop.Value.TryGetUInt64(out ulong uint64Value) => uint64Value, + JsonValueKind.Number when prop.Value.TryGetDateTime(out DateTime dateTimeValue) => dateTimeValue, + JsonValueKind.Number when prop.Value.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffsetValue) => dateTimeOffsetValue, + JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue, + JsonValueKind.Number => prop.Value.GetRawText(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Undefined => null, + JsonValueKind.Array => prop.Value, + JsonValueKind.Object => prop.Value, + _ => prop.Value + }; + } + + return dict; + } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs new file mode 100644 index 000000000..1c5e81d71 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Core.Rules.Models; + +public class RuleMessagePayload +{ + public string AgentId { get; set; } + public string TriggerName { get; set; } + public string Channel { get; set; } + public string Text { get; set; } + public Dictionary States { get; set; } + public DateTime Timestamp { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs new file mode 100644 index 000000000..0abea08b0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Core.Rules.Models; + +public class RuleTriggerActionRequest +{ + [JsonPropertyName("trigger_name")] + public string TriggerName { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + [JsonPropertyName("states")] + public IEnumerable? States { get; set; } + + [JsonPropertyName("options")] + public RuleTriggerOptions? Options { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index aee5a7fc7..e3403ed30 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -22,5 +22,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) // Register rule actions services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs index 903bcdb00..27ccbf1bf 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs @@ -13,17 +13,20 @@ public ChatRuleAction( _logger = logger; } - public string Name => "Chat"; + public string Name => "BotSharp-chat"; public async Task ExecuteAsync( Agent agent, IRuleTrigger trigger, RuleActionContext context) { + using var scope = _services.CreateScope(); + var sp = scope.ServiceProvider; + try { var channel = trigger.Channel; - var convService = _services.GetRequiredService(); + var convService = sp.GetRequiredService(); var conv = await convService.NewConversation(new Conversation { Channel = channel, @@ -40,7 +43,8 @@ public async Task ExecuteAsync( if (!context.States.IsNullOrEmpty()) { - allStates.AddRange(context.States!); + var states = context.States.Select(x => new MessageState(x.Key, x.Value)); + allStates.AddRange(states); } await convService.SetConversationId(conv.Id, allStates); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs new file mode 100644 index 000000000..4b7068fab --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs @@ -0,0 +1,47 @@ + +using BotSharp.Abstraction.Functions; + +namespace BotSharp.Core.Rules.Services; + +public class FunctionCallRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public FunctionCallRuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Name => "BotSharp-function-call"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + context.States ??= []; + + var funcName = context.States.TryGetValueOrDefault("function_name", string.Empty); + var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); + + if (func == null) + { + var errorMsg = $"Unable to find function '{funcName}' when running action {agent.Name}-{trigger.Name}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + var funcArg = context.States.TryGetValueOrDefault("function_argument") ?? new(); + await func.Execute(funcArg); + + return new RuleActionResult + { + Success = true, + Response = $"Function {funcName} is executed successfully." + }; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs new file mode 100644 index 000000000..6ce6898a8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -0,0 +1,191 @@ +using BotSharp.Abstraction.Options; +using System.Net.Mime; +using System.Text.Json; +using System.Web; + +namespace BotSharp.Core.Rules.Services; + +public sealed class HttpRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public HttpRuleAction( + IServiceProvider services, + ILogger logger, + IHttpClientFactory httpClientFactory) + { + _services = services; + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + public string Name => "BotSharp-http"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + context.States ??= []; + + var httpMethod = GetHttpMethod(context); + if (httpMethod == null) + { + var errorMsg = $"HTTP method is not supported in agent rule {agent.Name}-{trigger.Name}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + // Build the full URL + var fullUrl = BuildUrl(context); + + using var client = _httpClientFactory.CreateClient(); + + // Add headers + AddHttpHeaders(client, context); + + // Create request + var request = new HttpRequestMessage(httpMethod, fullUrl); + + // Add request body if provided + var requestBodyStr = GetHttpRequestBody(context); + if (!string.IsNullOrEmpty(requestBodyStr)) + { + request.Content = new StringContent(requestBodyStr, Encoding.UTF8, MediaTypeNames.Application.Json); + } + + _logger.LogInformation("Executing HTTP rule action for agent {AgentId}, URL: {Url}, Method: {Method}", + agent.Id, fullUrl, httpMethod); + + // Send request + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + _logger.LogInformation("HTTP rule action executed successfully for agent {AgentId}, Status: {StatusCode}", + agent.Id, response.StatusCode); + + return new RuleActionResult + { + Success = true, + Response = responseContent + }; + } + else + { + var errorMsg = $"HTTP request failed with status code {response.StatusCode}: {responseContent}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing HTTP rule action for agent {AgentId} and trigger {TriggerName}", + agent.Id, trigger.Name); + return RuleActionResult.Failed(ex.Message); + } + } + + private string BuildUrl(RuleActionContext context) + { + var url = context.States.TryGetValueOrDefault("http_url"); + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentNullException("Unable to find http_url in context"); + } + + // Fill in placeholders in url + var strValues = context.States.Where(x => x.Value != null && x.Value is string); + foreach (var item in strValues) + { + var value = item.Value as string; + if (string.IsNullOrEmpty(value)) + { + continue; + } + url.Replace($"{{{item.Key}}}", value); + } + + + var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); + + // Add query parameters + if (!queryParams.IsNullOrEmpty()) + { + var builder = new UriBuilder(url); + var query = HttpUtility.ParseQueryString(builder.Query); + + // Add new query params + foreach (var kv in queryParams!.Where(x => x.Value != null)) + { + query[kv.Key] = kv.Value!; + } + + // Assign merged query back + builder.Query = query.ToString(); + url = builder.ToString(); + } + + return url; + } + + private HttpMethod? GetHttpMethod(RuleActionContext context) + { + var method = context.States.TryGetValueOrDefault("http_method", string.Empty); + var innerMethod = method?.Trim()?.ToUpper(); + HttpMethod? matchMethod = null; + + switch (innerMethod) + { + case "GET": + matchMethod = HttpMethod.Get; + break; + case "POST": + matchMethod = HttpMethod.Post; + break; + case "DELETE": + matchMethod = HttpMethod.Delete; + break; + case "PUT": + matchMethod = HttpMethod.Put; + break; + case "PATCH": + matchMethod = HttpMethod.Patch; + break; + default: + break; + + } + + return matchMethod; + } + + private void AddHttpHeaders(HttpClient client, RuleActionContext context) + { + var headerParams = context.States.TryGetValueOrDefault>("http_headers"); + if (!headerParams.IsNullOrEmpty()) + { + foreach (var header in headerParams!) + { + client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + } + } + } + + private string? GetHttpRequestBody(RuleActionContext context) + { + var body = context.States.TryGetValueOrDefault("http_request_body"); + if (string.IsNullOrEmpty(body)) + { + return null; + } + + return JsonSerializer.Serialize(body, BotSharpOptions.defaultJsonOptions); + } +} + diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs new file mode 100644 index 000000000..4c0c72893 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs @@ -0,0 +1,125 @@ +using BotSharp.Core.Rules.Models; + +namespace BotSharp.Core.Rules.Services; + +public sealed class MessageQueueRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public MessageQueueRuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Name => "BotSharp-message-queue"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + context.States ??= []; + + // Get message queue service + var mqService = _services.GetService(); + if (mqService == null) + { + var errorMsg = "Message queue service is not configured. Please ensure a message queue provider (e.g., RabbitMQ) is registered."; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + // Create message payload + var payload = new RuleMessagePayload + { + AgentId = agent.Id, + TriggerName = trigger.Name, + Channel = trigger.Channel, + Text = context.Text, + Timestamp = DateTime.UtcNow, + States = context.States + }; + + // Publish message to queue + var mqOptions = GetMQPublishOptions(context); + var success = await mqService.PublishAsync(payload, mqOptions); + + if (success) + { + _logger.LogInformation("MessageQueue rule action executed successfully for agent {AgentId}", agent.Id); + return new RuleActionResult + { + Success = true, + Response = $"Message published to queue: {mqOptions.TopicName}-{mqOptions.RoutingKey}" + }; + } + else + { + var errorMsg = $"Failed to publish message to queue {mqOptions.TopicName}-{mqOptions.RoutingKey}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing MessageQueue rule action for agent {AgentId} and trigger {TriggerName}", + agent.Id, trigger.Name); + return RuleActionResult.Failed(ex.Message); + } + } + + private MQPublishOptions GetMQPublishOptions(RuleActionContext context) + { + var topicName = context.States.TryGetValueOrDefault("mq_topic_name", string.Empty); + var routingKey = context.States.TryGetValueOrDefault("mq_routing_key", string.Empty); + var delayMilliseconds = ParseDelay(context); + + return new MQPublishOptions + { + TopicName = topicName, + RoutingKey = routingKey, + DelayMilliseconds = delayMilliseconds + }; + } + + private long ParseDelay(RuleActionContext context) + { + var qty = (double)context.States.TryGetValueOrDefault("mq_delay_qty", 0); + if (qty == 0) + { + qty = context.States.TryGetValueOrDefault("mq_delay_qty", 0.0); + } + + var unit = context.States.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; + unit = unit.ToLower(); + + var milliseconds = 0L; + switch (unit) + { + case "second": + case "seconds": + milliseconds = (long)TimeSpan.FromSeconds(qty).TotalMilliseconds; + break; + case "minute": + case "minutes": + milliseconds = (long)TimeSpan.FromMilliseconds(qty).TotalMilliseconds; + break; + case "hour": + case "hours": + milliseconds = (long)TimeSpan.FromHours(qty).TotalMilliseconds; + break; + case "day": + case "days": + milliseconds = (long)TimeSpan.FromDays(qty).TotalMilliseconds; + break; + } + + return milliseconds; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index 2cfb617d2..19982448e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -1,6 +1,7 @@ global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; +global using System.Text; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Plugins; @@ -13,6 +14,7 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Infrastructures.MessageQueues; +global using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Repositories.Filters; global using BotSharp.Abstraction.Rules; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 52fd719fd..0e4cc9dd0 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Rules; namespace BotSharp.OpenAPI.Controllers; @@ -18,9 +17,9 @@ public IEnumerable GetRuleTriggers() }).OrderBy(x => x.TriggerName); } - [HttpGet("/rule/formalization")] - public async Task GetFormalizedRuleDefinition([FromBody] AgentRule rule) + [HttpGet("/rule/actions")] + public async Task> GetRuleActions() { - return "{}"; + return _services.GetServices().Select(x => x.Name).OrderBy(x => x); } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 4205fdc46..0fd34fa6e 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -8,6 +9,8 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public string Criteria { get; set; } = default!; + public string? Action { get; set; } + public BsonDocument? ActionConfig { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -15,7 +18,9 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria + Criteria = rule.Criteria, + Action = rule.Action, + ActionConfig = rule.ActionConfig != null ? BsonDocument.Parse(rule.ActionConfig.RootElement.GetRawText()) : null }; } @@ -25,7 +30,9 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria + Criteria = rule.Criteria, + Action = rule.Action, + ActionConfig = rule.ActionConfig != null ? JsonDocument.Parse(rule.ActionConfig.ToJson()) : null }; } } From 9341685421f856cc0d8bd3a08b80846a6b27960b Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 14:30:49 -0600 Subject: [PATCH 23/36] replace url --- .../BotSharp.Core.Rules/Services/HttpRuleAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs index 6ce6898a8..c7a2611f3 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -108,7 +108,7 @@ private string BuildUrl(RuleActionContext context) { continue; } - url.Replace($"{{{item.Key}}}", value); + url = url.Replace($"{{{item.Key}}}", value); } From dc5e2e62294d2d7db0a21110e632daaf8910ef9e Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 14:35:43 -0600 Subject: [PATCH 24/36] avoid negative delay qty --- .../BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs index 4c0c72893..b32f25e3c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs @@ -96,6 +96,11 @@ private long ParseDelay(RuleActionContext context) qty = context.States.TryGetValueOrDefault("mq_delay_qty", 0.0); } + if (qty <= 0) + { + return 0L; + } + var unit = context.States.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; unit = unit.ToLower(); From f73598be1a698de9df756fe2dd02ddae88e328ce Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 15:30:36 -0600 Subject: [PATCH 25/36] add agent rule action --- .../Agents/Models/AgentRule.cs | 18 +++++-- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 17 ++++--- .../Models/AgentRuleMongoElement.cs | 47 ++++++++++++++++--- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 6e31e1b71..2e87d918a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -14,7 +14,17 @@ public class AgentRule public string Criteria { get; set; } = string.Empty; [JsonPropertyName("action")] - public string? Action { get; set; } + public AgentRuleAction? Action { get; set; } +} + + +public class AgentRuleAction +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("disabled")] + public bool Disabled { get; set; } /// /// Adaptive configuration for rule actions. @@ -24,7 +34,7 @@ public class AgentRule /// - For "MessageQueue" action: contains mq_config with topic_name, routing_key, etc. /// - For custom actions: can contain any custom configuration structure /// - [JsonPropertyName("action_config")] + [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonDocument? ActionConfig { get; set; } -} + public JsonDocument? Config { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index a485e124e..e49ac968e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -55,7 +55,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundRule == null) + if (foundRule == null || foundRule.Action?.Disabled == false) { continue; } @@ -63,9 +63,11 @@ public async Task> Triggered(IRuleTrigger trigger, string te var context = new RuleActionContext { Text = text, - States = BuildRuleActionContext(foundRule, states) + States = BuildRuleActionContext(foundRule.Action, states) }; - var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("BotSharp-chat")!, context); + + var action = foundRule?.Action?.Name ?? "BotSharp-chat"; + var result = await ExecuteActionAsync(agent, trigger, action, context); if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { newConversationIds.Add(result.ConversationId); @@ -75,13 +77,13 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } - private Dictionary BuildRuleActionContext(AgentRule rule, IEnumerable? states) + private Dictionary BuildRuleActionContext(AgentRuleAction? ruleAction, IEnumerable? states) { var dict = new Dictionary(); - if (rule.ActionConfig != null) + if (ruleAction?.Config != null) { - dict = ConvertToDictionary(rule.ActionConfig); + dict = ConvertToDictionary(ruleAction.Config); } if (!states.IsNullOrEmpty()) @@ -119,9 +121,6 @@ private async Task ExecuteActionAsync( _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", action.Name, agent.Id, trigger.Name); - // Combine states - - var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 0fd34fa6e..dc1c4671b 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -9,8 +9,7 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public string Criteria { get; set; } = default!; - public string? Action { get; set; } - public BsonDocument? ActionConfig { get; set; } + public AgentRuleActionMongModel? Action { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -19,8 +18,7 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = rule.Action, - ActionConfig = rule.ActionConfig != null ? BsonDocument.Parse(rule.ActionConfig.RootElement.GetRawText()) : null + Action = AgentRuleActionMongModel.ToMongoModel(rule.Action) }; } @@ -31,8 +29,45 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = rule.Action, - ActionConfig = rule.ActionConfig != null ? JsonDocument.Parse(rule.ActionConfig.ToJson()) : null + Action = AgentRuleActionMongModel.ToDomainModel(rule.Action) }; } } + + +public class AgentRuleActionMongModel +{ + public string Name { get; set; } + public bool Disabled { get; set; } + public BsonDocument? Config { get; set; } + + public static AgentRuleActionMongModel? ToMongoModel(AgentRuleAction? action) + { + if (action == null) + { + return null; + } + + return new AgentRuleActionMongModel + { + Name = action.Name, + Disabled = action.Disabled, + Config = action.Config != null ? BsonDocument.Parse(action.Config.RootElement.GetRawText()) : null + }; + } + + public static AgentRuleAction? ToDomainModel(AgentRuleActionMongModel? action) + { + if (action == null) + { + return null; + } + + return new AgentRuleAction + { + Name = action.Name, + Disabled = action.Disabled, + Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null + }; + } +} \ No newline at end of file From 2ea54831684adca384baa25285c52738fbfc69aa Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 16:32:35 -0600 Subject: [PATCH 26/36] ignore action when null --- .../BotSharp.Abstraction/Agents/Models/AgentRule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 2e87d918a..07ae67b77 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -14,6 +14,7 @@ public class AgentRule public string Criteria { get; set; } = string.Empty; [JsonPropertyName("action")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentRuleAction? Action { get; set; } } From 3d1d0eaff3653272412da308af23b4b5baaaf69e Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 16:45:59 -0600 Subject: [PATCH 27/36] return function calling response --- .../BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs index 4b7068fab..be24697cf 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs @@ -41,7 +41,7 @@ public async Task ExecuteAsync( return new RuleActionResult { Success = true, - Response = $"Function {funcName} is executed successfully." + Response = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content }; } } From 4b7f5ac47fe22037202e1118bc5b1c96d7638571 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 27 Jan 2026 12:09:14 -0600 Subject: [PATCH 28/36] refine http rule --- .../Rules/Hooks/RuleTriggerHookBase.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 2 +- .../BotSharp.Core.Rules/Services/HttpRuleAction.cs | 9 +++------ .../Models/AgentRuleMongoElement.cs | 2 +- src/WebStarter/appsettings.json | 1 + 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs index f3b99cbe8..44b85a29f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs @@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Rules.Hooks; public class RuleTriggerHookBase : IRuleTriggerHook { - public string SelfId = string.Empty; + public string SelfId => string.Empty; public Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index e49ac968e..6cbf00f6b 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -55,7 +55,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundRule == null || foundRule.Action?.Disabled == false) + if (foundRule == null || foundRule.Action?.Disabled == true) { continue; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs index c7a2611f3..5b2e0a6d8 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -100,10 +100,9 @@ private string BuildUrl(RuleActionContext context) } // Fill in placeholders in url - var strValues = context.States.Where(x => x.Value != null && x.Value is string); - foreach (var item in strValues) + foreach (var item in context.States) { - var value = item.Value as string; + var value = item.Value?.ToString(); if (string.IsNullOrEmpty(value)) { continue; @@ -111,10 +110,8 @@ private string BuildUrl(RuleActionContext context) url = url.Replace($"{{{item.Key}}}", value); } - - var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); - // Add query parameters + var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); if (!queryParams.IsNullOrEmpty()) { var builder = new UriBuilder(url); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index dc1c4671b..ce25d121f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -34,7 +34,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) } } - +[BsonIgnoreExtraElements(Inherited = true)] public class AgentRuleActionMongModel { public string Name { get; set; } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 322315192..b3c2b35e9 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1039,6 +1039,7 @@ "BotSharp.Core.A2A", "BotSharp.Core.SideCar", "BotSharp.Core.Crontab", + "BotSharp.Core.Rules", "BotSharp.Core.Realtime", "BotSharp.Logger", "BotSharp.Plugin.MongoStorage", From 0b97b67a5071d2c09f6b300339641de0d35a4cd6 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 27 Jan 2026 19:12:06 -0600 Subject: [PATCH 29/36] add agent filter to rule trigger options --- .../Rules/Options/RuleTriggerOptions.cs | 7 +++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 20 ++++++++++++------- .../Services/FunctionCallRuleAction.cs | 2 -- .../Services/HttpRuleAction.cs | 6 ++---- .../Services/MessageQueueRuleAction.cs | 2 -- .../Services/RuleCriteria.cs | 2 +- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 70567ea6d..e01513f51 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,7 +1,14 @@ +using BotSharp.Abstraction.Repositories.Filters; + namespace BotSharp.Abstraction.Rules.Options; public class RuleTriggerOptions { + /// + /// Filter agents + /// + public AgentFilter? AgentFilter { get; set; } + /// /// Criteria options for validating whether the rule should be triggered /// diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6cbf00f6b..a5e5e7b6a 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -22,7 +22,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Pull all user defined rules var agentService = _services.GetRequiredService(); - var agents = await agentService.GetAgents(new AgentFilter + var agents = await agentService.GetAgents(options?.AgentFilter ?? new AgentFilter { Pager = new Pagination { @@ -34,11 +34,17 @@ public async Task> Triggered(IRuleTrigger trigger, string te var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); foreach (var agent in filteredAgents) { + var rule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); + if (rule == null) + { + continue; + } + // Criteria validation if (options?.Criteria != null) { var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "botsharp-rule-criteria")); + .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "BotSharp-rule-criteria")); if (criteria == null) { @@ -49,13 +55,12 @@ public async Task> Triggered(IRuleTrigger trigger, string te var isValid = await criteria.ValidateAsync(agent, trigger, options.Criteria); if (!isValid) { - _logger.LogDebug("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); + _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; } } - var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundRule == null || foundRule.Action?.Disabled == true) + if (rule.Action?.Disabled == true) { continue; } @@ -63,10 +68,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te var context = new RuleActionContext { Text = text, - States = BuildRuleActionContext(foundRule.Action, states) + States = BuildRuleActionContext(rule.Action, states) }; - var action = foundRule?.Action?.Name ?? "BotSharp-chat"; + var action = rule?.Action?.Name ?? "BotSharp-chat"; var result = await ExecuteActionAsync(agent, trigger, action, context); if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { @@ -128,6 +133,7 @@ private async Task ExecuteActionAsync( } // Execute action + context.States ??= []; var result = await action.ExecuteAsync(agent, trigger, context); foreach (var hook in hooks) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs index be24697cf..808b4a182 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs @@ -23,8 +23,6 @@ public async Task ExecuteAsync( IRuleTrigger trigger, RuleActionContext context) { - context.States ??= []; - var funcName = context.States.TryGetValueOrDefault("function_name", string.Empty); var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs index 5b2e0a6d8..042cd6d4b 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -30,8 +30,6 @@ public async Task ExecuteAsync( { try { - context.States ??= []; - var httpMethod = GetHttpMethod(context); if (httpMethod == null) { @@ -176,8 +174,8 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) private string? GetHttpRequestBody(RuleActionContext context) { - var body = context.States.TryGetValueOrDefault("http_request_body"); - if (string.IsNullOrEmpty(body)) + var body = context.States.GetValueOrDefault("http_request_body"); + if (body == null) { return null; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs index b32f25e3c..403b63af8 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs @@ -24,8 +24,6 @@ public async Task ExecuteAsync( { try { - context.States ??= []; - // Get message queue service var mqService = _services.GetService(); if (mqService == null) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs index 015c6e910..c5802385c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs @@ -18,7 +18,7 @@ public RuleCriteria( _codingSettings = codingSettings; } - public string Provider => "botsharp-rule-criteria"; + public string Provider => "BotSharp-rule-criteria"; public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) { From cd68f58409459382d76d435caf71d44fd96d51c4 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 28 Jan 2026 12:01:34 -0600 Subject: [PATCH 30/36] refine rule criteria --- .../Agents/Models/AgentRule.cs | 42 ++++-- .../Rules/Hooks/IRuleTriggerHook.cs | 3 + .../Rules/Hooks/RuleTriggerHookBase.cs | 18 --- .../Rules/IRuleCriteria.cs | 6 +- .../Rules/Models/RuleActionContext.cs | 2 +- .../Rules/Models/RuleCriteriaContext.cs | 7 + .../Rules/Models/RuleCriteriaResult.cs | 19 +++ .../Rules/Options/RuleCriteriaOptions.cs | 5 + .../Rules/Options/RuleTriggerOptions.cs | 5 - .../Constants/RuleConstant.cs | 7 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 123 ++++++++++++------ .../BotSharp.Core.Rules/RulesPlugin.cs | 5 +- .../Services/{ => Actions}/ChatRuleAction.cs | 8 +- .../{ => Actions}/FunctionCallRuleAction.cs | 7 +- .../Services/{ => Actions}/HttpRuleAction.cs | 18 +-- .../{ => Actions}/MessageQueueRuleAction.cs | 14 +- .../CodeScriptRuleCriteria.cs} | 49 +++---- .../BotSharp.Core.Rules/Using.cs | 3 + .../Models/AgentRuleMongoElement.cs | 62 +++++++-- 19 files changed, 271 insertions(+), 132 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/ChatRuleAction.cs (88%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/FunctionCallRuleAction.cs (82%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/HttpRuleAction.cs (88%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/MessageQueueRuleAction.cs (87%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{RuleCriteria.cs => Criteria/CodeScriptRuleCriteria.cs} (68%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 07ae67b77..635b1628a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -13,20 +13,29 @@ public class AgentRule [JsonPropertyName("criteria")] public string Criteria { get; set; } = string.Empty; - [JsonPropertyName("action")] + [JsonPropertyName("rule_criteria")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public AgentRuleAction? Action { get; set; } -} + public AgentRuleCriteria? RuleCriteria { get; set; } + [JsonPropertyName("rule_action")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AgentRuleAction? RuleAction { get; set; } +} -public class AgentRuleAction +public class AgentRuleCriteria : AgentRuleConfigBase { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("disabled")] - public bool Disabled { get; set; } + /// + /// Adaptive configuration for rule criteria. + /// This flexible JSON document can store any criteria-specific configuration. + /// The structure depends on the criteria executor + /// + [JsonPropertyName("config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public override JsonDocument? Config { get; set; } +} +public class AgentRuleAction : AgentRuleConfigBase +{ /// /// Adaptive configuration for rule actions. /// This flexible JSON document can store any action-specific configuration. @@ -37,5 +46,18 @@ public class AgentRuleAction /// [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonDocument? Config { get; set; } + public override JsonDocument? Config { get; set; } +} + +public class AgentRuleConfigBase +{ + [JsonPropertyName("name")] + public virtual string Name { get; set; } + + [JsonPropertyName("disabled")] + public virtual bool Disabled { get; set; } + + [JsonPropertyName("config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public virtual JsonDocument? Config { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 08195c89c..9e9817890 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -5,6 +5,9 @@ namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { + Task BeforeRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; + Task AfterRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs deleted file mode 100644 index 44b85a29f..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -using BotSharp.Abstraction.Rules.Models; - -namespace BotSharp.Abstraction.Rules.Hooks; - -public class RuleTriggerHookBase : IRuleTriggerHook -{ - public string SelfId => string.Empty; - - public Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) - { - return Task.CompletedTask; - } - - public Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) - { - return Task.CompletedTask; - } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs index af5d5cf3d..f94e0b36f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs @@ -1,9 +1,11 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules; public interface IRuleCriteria { string Provider { get; } - Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) - => Task.FromResult(false); + Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) + => Task.FromResult(new RuleCriteriaResult()); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 12a0a3e66..ca9c6045c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -3,5 +3,5 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { public string Text { get; set; } = string.Empty; - public Dictionary States { get; set; } = []; + public Dictionary Parameters { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs new file mode 100644 index 000000000..d777db1d2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleCriteriaContext +{ + public string Text { get; set; } = string.Empty; + public Dictionary Parameters { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs new file mode 100644 index 000000000..fc1df6ceb --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleCriteriaResult +{ + /// + /// Whether the criteria executed successfully + /// + public bool Success { get; set; } + + /// + /// Response content from the action + /// + public bool IsValid { get; set; } + + /// + /// Error message if the criteria failed + /// + public string? ErrorMessage { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs index 30c820f97..27b5167f4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs @@ -31,4 +31,9 @@ public class CriteriaExecuteOptions /// Json arguments as an input value to the code script /// public JsonDocument? ArgumentContent { get; set; } + + /// + /// Custom parameters + /// + public Dictionary Parameters { get; set; } = []; } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index e01513f51..a2ab78c85 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -8,9 +8,4 @@ public class RuleTriggerOptions /// Filter agents /// public AgentFilter? AgentFilter { get; set; } - - /// - /// Criteria options for validating whether the rule should be triggered - /// - public RuleCriteriaOptions? Criteria { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs new file mode 100644 index 000000000..e9c180120 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Core.Rules.Constants; + +public static class RuleConstant +{ + public const string DEFAULT_CRITERIA_PROVIDER = "BotSharp-code-script"; + public const string DEFAULT_ACTION_NAME = "BotSharp-chat"; +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index a5e5e7b6a..4241c220d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,4 +1,4 @@ -using BotSharp.Abstraction.Rules.Hooks; +using System.Data; using System.Text.Json; namespace BotSharp.Core.Rules.Engines; @@ -41,67 +41,92 @@ public async Task> Triggered(IRuleTrigger trigger, string te } // Criteria validation - if (options?.Criteria != null) + if (rule.RuleCriteria != null && !rule.RuleCriteria.Disabled) { - var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "BotSharp-rule-criteria")); - - if (criteria == null) + var criteriaContext = new RuleCriteriaContext { - _logger.LogWarning("No criteria provider found for {Provider}, skipping agent {AgentId}", options.Criteria.Provider, agent.Id); - continue; - } - - var isValid = await criteria.ValidateAsync(agent, trigger, options.Criteria); - if (!isValid) + Text = text, + Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) + }; + var criteriaResult = await ExecuteCriteriaAsync(agent, trigger, rule.RuleCriteria?.Name, criteriaContext); + if (criteriaResult == null || !criteriaResult.IsValid) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; } } - - if (rule.Action?.Disabled == true) + + // Execute action + if (rule.RuleAction?.Disabled == true) { - continue; + continue; } - var context = new RuleActionContext + var actionContext = new RuleActionContext { Text = text, - States = BuildRuleActionContext(rule.Action, states) + Parameters = BuildContextParameters(rule.RuleAction?.Config, states) }; - var action = rule?.Action?.Name ?? "BotSharp-chat"; - var result = await ExecuteActionAsync(agent, trigger, action, context); - if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) + var action = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; + var actionResult = await ExecuteActionAsync(agent, trigger, action, actionContext); + if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { - newConversationIds.Add(result.ConversationId); + newConversationIds.Add(actionResult.ConversationId); } } return newConversationIds; } - private Dictionary BuildRuleActionContext(AgentRuleAction? ruleAction, IEnumerable? states) - { - var dict = new Dictionary(); - if (ruleAction?.Config != null) - { - dict = ConvertToDictionary(ruleAction.Config); - } - - if (!states.IsNullOrEmpty()) + #region Criteria + private async Task ExecuteCriteriaAsync( + Agent agent, + IRuleTrigger trigger, + string? criteriaProvider, + RuleCriteriaContext context) + { + try { - foreach (var state in states!) + var criteria = _services.GetServices() + .FirstOrDefault(x => x.Provider == criteriaProvider); + + if (criteria == null) { - dict[state.Key] = state.Value; + return null; + } + + _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", + criteria.Provider, agent.Id, trigger.Name); + + var hooks = _services.GetHooks(agent.Id); + foreach (var hook in hooks) + { + await hook.BeforeRuleCriteriaExecuted(agent, trigger, context); } + + // Execute criteria + context.Parameters ??= []; + var result = await criteria.ValidateAsync(agent, trigger, context); + + foreach (var hook in hooks) + { + await hook.AfterRuleCriteriaExecuted(agent, trigger, result); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", criteriaProvider ?? string.Empty, agent.Id); + return null; } - - return dict; } + #endregion + + #region Action private async Task ExecuteActionAsync( Agent agent, IRuleTrigger trigger, @@ -133,8 +158,8 @@ private async Task ExecuteActionAsync( } // Execute action - context.States ??= []; - var result = await action.ExecuteAsync(agent, trigger, context); + context.Parameters ??= []; + var result = await action.ExecuteAsync(agent, trigger, context); foreach (var hook in hooks) { @@ -149,8 +174,31 @@ private async Task ExecuteActionAsync( return RuleActionResult.Failed(ex.Message); } } + #endregion + + + #region Private methods + private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states) + { + var dict = new Dictionary(); + + if (config != null) + { + dict = ConvertToDictionary(config); + } + + if (!states.IsNullOrEmpty()) + { + foreach (var state in states!) + { + dict[state.Key] = state.Value; + } + } + + return dict; + } - public static Dictionary ConvertToDictionary(JsonDocument doc) + private Dictionary ConvertToDictionary(JsonDocument doc) { var dict = new Dictionary(); @@ -183,5 +231,6 @@ JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue } return dict; + #endregion } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index e3403ed30..5b8ade6b0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,5 +1,6 @@ using BotSharp.Core.Rules.Engines; -using BotSharp.Core.Rules.Services; +using BotSharp.Core.Rules.Services.Actions; +using BotSharp.Core.Rules.Services.Criteria; namespace BotSharp.Core.Rules; @@ -18,7 +19,7 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { services.AddScoped(); - services.AddScoped(); + services.AddScoped(); // Register rule actions services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs similarity index 88% rename from src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs index 27ccbf1bf..4c599a3e9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public sealed class ChatRuleAction : IRuleAction { @@ -13,7 +13,7 @@ public ChatRuleAction( _logger = logger; } - public string Name => "BotSharp-chat"; + public string Name => RuleConstant.DEFAULT_ACTION_NAME; public async Task ExecuteAsync( Agent agent, @@ -41,9 +41,9 @@ public async Task ExecuteAsync( new("channel", channel) }; - if (!context.States.IsNullOrEmpty()) + if (!context.Parameters.IsNullOrEmpty()) { - var states = context.States.Select(x => new MessageState(x.Key, x.Value)); + var states = context.Parameters.Select(x => new MessageState(x.Key, x.Value)); allStates.AddRange(states); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs similarity index 82% rename from src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs index 808b4a182..6e01f71f4 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs @@ -1,7 +1,6 @@ - using BotSharp.Abstraction.Functions; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public class FunctionCallRuleAction : IRuleAction { @@ -23,7 +22,7 @@ public async Task ExecuteAsync( IRuleTrigger trigger, RuleActionContext context) { - var funcName = context.States.TryGetValueOrDefault("function_name", string.Empty); + var funcName = context.Parameters.TryGetValueOrDefault("function_name", string.Empty); var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); if (func == null) @@ -33,7 +32,7 @@ public async Task ExecuteAsync( return RuleActionResult.Failed(errorMsg); } - var funcArg = context.States.TryGetValueOrDefault("function_argument") ?? new(); + var funcArg = context.Parameters.TryGetValueOrDefault("function_argument") ?? new(); await func.Execute(funcArg); return new RuleActionResult diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs similarity index 88% rename from src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs index 042cd6d4b..f1d2163ce 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Web; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public sealed class HttpRuleAction : IRuleAction { @@ -91,25 +91,25 @@ public async Task ExecuteAsync( private string BuildUrl(RuleActionContext context) { - var url = context.States.TryGetValueOrDefault("http_url"); + var url = context.Parameters.TryGetValueOrDefault("http_url"); if (string.IsNullOrEmpty(url)) { throw new ArgumentNullException("Unable to find http_url in context"); } // Fill in placeholders in url - foreach (var item in context.States) + foreach (var param in context.Parameters) { - var value = item.Value?.ToString(); + var value = param.Value?.ToString(); if (string.IsNullOrEmpty(value)) { continue; } - url = url.Replace($"{{{item.Key}}}", value); + url = url.Replace($"{{{param.Key}}}", value); } // Add query parameters - var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); + var queryParams = context.Parameters.TryGetValueOrDefault>("http_query_params"); if (!queryParams.IsNullOrEmpty()) { var builder = new UriBuilder(url); @@ -131,7 +131,7 @@ private string BuildUrl(RuleActionContext context) private HttpMethod? GetHttpMethod(RuleActionContext context) { - var method = context.States.TryGetValueOrDefault("http_method", string.Empty); + var method = context.Parameters.TryGetValueOrDefault("http_method", string.Empty); var innerMethod = method?.Trim()?.ToUpper(); HttpMethod? matchMethod = null; @@ -162,7 +162,7 @@ private string BuildUrl(RuleActionContext context) private void AddHttpHeaders(HttpClient client, RuleActionContext context) { - var headerParams = context.States.TryGetValueOrDefault>("http_headers"); + var headerParams = context.Parameters.TryGetValueOrDefault>("http_headers"); if (!headerParams.IsNullOrEmpty()) { foreach (var header in headerParams!) @@ -174,7 +174,7 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) private string? GetHttpRequestBody(RuleActionContext context) { - var body = context.States.GetValueOrDefault("http_request_body"); + var body = context.Parameters.GetValueOrDefault("http_request_body"); if (body == null) { return null; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs similarity index 87% rename from src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs index 403b63af8..fcd406b55 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs @@ -1,6 +1,6 @@ using BotSharp.Core.Rules.Models; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public sealed class MessageQueueRuleAction : IRuleAction { @@ -41,7 +41,7 @@ public async Task ExecuteAsync( Channel = trigger.Channel, Text = context.Text, Timestamp = DateTime.UtcNow, - States = context.States + States = context.Parameters }; // Publish message to queue @@ -74,8 +74,8 @@ public async Task ExecuteAsync( private MQPublishOptions GetMQPublishOptions(RuleActionContext context) { - var topicName = context.States.TryGetValueOrDefault("mq_topic_name", string.Empty); - var routingKey = context.States.TryGetValueOrDefault("mq_routing_key", string.Empty); + var topicName = context.Parameters.TryGetValueOrDefault("mq_topic_name", string.Empty); + var routingKey = context.Parameters.TryGetValueOrDefault("mq_routing_key", string.Empty); var delayMilliseconds = ParseDelay(context); return new MQPublishOptions @@ -88,10 +88,10 @@ private MQPublishOptions GetMQPublishOptions(RuleActionContext context) private long ParseDelay(RuleActionContext context) { - var qty = (double)context.States.TryGetValueOrDefault("mq_delay_qty", 0); + var qty = (double)context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0); if (qty == 0) { - qty = context.States.TryGetValueOrDefault("mq_delay_qty", 0.0); + qty = context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0.0); } if (qty <= 0) @@ -99,7 +99,7 @@ private long ParseDelay(RuleActionContext context) return 0L; } - var unit = context.States.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; + var unit = context.Parameters.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; unit = unit.ToLower(); var milliseconds = 0L; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs similarity index 68% rename from src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs index c5802385c..ad3489bc6 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs @@ -1,16 +1,16 @@ using System.Text.Json; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Criteria; -public class RuleCriteria : IRuleCriteria +public class CodeScriptRuleCriteria : IRuleCriteria { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly CodingSettings _codingSettings; - public RuleCriteria( + public CodeScriptRuleCriteria( IServiceProvider services, - ILogger logger, + ILogger logger, CodingSettings codingSettings) { _services = services; @@ -18,41 +18,45 @@ public RuleCriteria( _codingSettings = codingSettings; } - public string Provider => "BotSharp-rule-criteria"; + public string Provider => RuleConstant.DEFAULT_CRITERIA_PROVIDER; - public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) + public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) { + var result = new RuleCriteriaResult(); + if (string.IsNullOrWhiteSpace(agent?.Id)) { - return false; + return result; } - var provider = options.CodeProcessor ?? BuiltInCodeProcessor.PyInterpreter; + var provider = context.Parameters.TryGetValueOrDefault("code_processor") ?? BuiltInCodeProcessor.PyInterpreter; var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); if (processor == null) { _logger.LogWarning($"Unable to find code processor: {provider}."); - return false; + return result; } var agentService = _services.GetRequiredService(); - var scriptName = options.CodeScriptName ?? $"{trigger.Name}_rule.py"; + var scriptName = context.Parameters.TryGetValueOrDefault("code_script_name") ?? $"{trigger.Name}_rule.py"; var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; + var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name})."; if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) { _logger.LogWarning($"Unable to find {msg}."); - return false; + return result; } try { var hooks = _services.GetHooks(agent.Id); - var arguments = BuildArguments(options.ArgumentName, options.ArgumentContent); - var context = new CodeExecutionContext + var argName = context.Parameters.TryGetValueOrDefault("argument_name"); + var argValue = context.Parameters.TryGetValueOrDefault("argument_value"); + var arguments = BuildArguments(argName, argValue); + var codeExeContext = new CodeExecutionContext { CodeScript = codeScript, Arguments = arguments @@ -60,7 +64,7 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, Criteri foreach (var hook in hooks) { - await hook.BeforeCodeExecution(agent, context); + await hook.BeforeCodeExecution(agent, codeExeContext); } var (useLock, useProcess, timeoutSeconds) = CodingUtil.GetCodeExecutionConfig(_codingSettings); @@ -89,20 +93,19 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, Criteri if (response == null || !response.Success) { _logger.LogWarning($"Failed to handle {msg}"); - return false; + return result; } - bool result; LogLevel logLevel; if (response.Result.IsEqualTo("true")) { logLevel = LogLevel.Information; - result = true; + result.Success = true; + result.IsValid = true; } else { logLevel = LogLevel.Warning; - result = false; } _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); @@ -111,16 +114,16 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, Criteri catch (Exception ex) { _logger.LogError(ex, $"Error when handling {msg}"); - return false; + return result; } } - private List BuildArguments(string? name, JsonDocument? args) + private List BuildArguments(string? name, JsonElement? args) { var keyValues = new List(); if (args != null) { - keyValues.Add(new KeyValue(name ?? "trigger_args", args.RootElement.GetRawText())); + keyValues.Add(new KeyValue(name ?? "trigger_args", args.Value.GetRawText())); } return keyValues; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index 19982448e..2d1dc6844 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -20,6 +20,7 @@ global using BotSharp.Abstraction.Rules; global using BotSharp.Abstraction.Rules.Options; global using BotSharp.Abstraction.Rules.Models; +global using BotSharp.Abstraction.Rules.Hooks; global using BotSharp.Abstraction.Utilities; global using BotSharp.Abstraction.Coding; global using BotSharp.Abstraction.Coding.Contexts; @@ -28,3 +29,5 @@ global using BotSharp.Abstraction.Coding.Utils; global using BotSharp.Abstraction.Coding.Settings; global using BotSharp.Abstraction.Hooks; + +global using BotSharp.Core.Rules.Constants; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index ce25d121f..a2cfb4e3e 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using System; using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -9,7 +10,8 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public string Criteria { get; set; } = default!; - public AgentRuleActionMongModel? Action { get; set; } + public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } + public AgentRuleActionMongoModel? RuleAction { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -18,7 +20,8 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = AgentRuleActionMongModel.ToMongoModel(rule.Action) + RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria), + RuleAction = AgentRuleActionMongoModel.ToMongoModel(rule.RuleAction) }; } @@ -29,26 +32,57 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = AgentRuleActionMongModel.ToDomainModel(rule.Action) + RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria), + RuleAction = AgentRuleActionMongoModel.ToDomainModel(rule.RuleAction) }; } } [BsonIgnoreExtraElements(Inherited = true)] -public class AgentRuleActionMongModel +public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel { - public string Name { get; set; } - public bool Disabled { get; set; } - public BsonDocument? Config { get; set; } + public static AgentRuleCriteriaMongoModel? ToMongoModel(AgentRuleCriteria? criteria) + { + if (criteria == null) + { + return null; + } + + return new AgentRuleCriteriaMongoModel + { + Name = criteria.Name, + Disabled = criteria.Disabled, + Config = criteria.Config != null ? BsonDocument.Parse(criteria.Config.RootElement.GetRawText()) : null + }; + } + + public static AgentRuleCriteria? ToDomainModel(AgentRuleCriteriaMongoModel? criteria) + { + if (criteria == null) + { + return null; + } + + return new AgentRuleCriteria + { + Name = criteria.Name, + Disabled = criteria.Disabled, + Config = criteria.Config != null ? JsonDocument.Parse(criteria.Config.ToJson()) : null + }; + } +} - public static AgentRuleActionMongModel? ToMongoModel(AgentRuleAction? action) +[BsonIgnoreExtraElements(Inherited = true)] +public class AgentRuleActionMongoModel : AgentRuleConfigMongoModel +{ + public static AgentRuleActionMongoModel? ToMongoModel(AgentRuleAction? action) { if (action == null) { return null; } - return new AgentRuleActionMongModel + return new AgentRuleActionMongoModel { Name = action.Name, Disabled = action.Disabled, @@ -56,7 +90,7 @@ public class AgentRuleActionMongModel }; } - public static AgentRuleAction? ToDomainModel(AgentRuleActionMongModel? action) + public static AgentRuleAction? ToDomainModel(AgentRuleActionMongoModel? action) { if (action == null) { @@ -70,4 +104,12 @@ public class AgentRuleActionMongModel Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null }; } +} + +[BsonIgnoreExtraElements(Inherited = true)] +public class AgentRuleConfigMongoModel +{ + public string Name { get; set; } + public bool Disabled { get; set; } + public BsonDocument? Config { get; set; } } \ No newline at end of file From a318c7a167299c846d844370a463333761ca1730 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 28 Jan 2026 14:36:36 -0600 Subject: [PATCH 31/36] refine agent rule structure --- .../Agents/Models/AgentRule.cs | 10 +++- .../Controllers/RuleController.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 56 +++++++++++-------- .../Controllers/Agent/AgentController.Rule.cs | 6 ++ .../Models/AgentRuleMongoElement.cs | 8 +-- 5 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 635b1628a..e0411801e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -10,9 +10,6 @@ public class AgentRule [JsonPropertyName("disabled")] public bool Disabled { get; set; } - [JsonPropertyName("criteria")] - public string Criteria { get; set; } = string.Empty; - [JsonPropertyName("rule_criteria")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentRuleCriteria? RuleCriteria { get; set; } @@ -24,6 +21,13 @@ public class AgentRule public class AgentRuleCriteria : AgentRuleConfigBase { + /// + /// Criteria + /// + [JsonPropertyName("criteria_text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string CriteriaText { get; set; } = string.Empty; + /// /// Adaptive configuration for rule criteria. /// This flexible JSON document can store any criteria-specific configuration. diff --git a/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs index ebb2bf0c5..feb314ef7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs @@ -22,7 +22,7 @@ public RuleController( _ruleEngine = ruleEngine; } - [HttpPost("/rule/trigger/run")] + [HttpPost("/rule/trigger/action")] public async Task RunAction([FromBody] RuleTriggerActionRequest request) { if (request == null) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 4241c220d..a078f3542 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -41,15 +41,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te } // Criteria validation - if (rule.RuleCriteria != null && !rule.RuleCriteria.Disabled) + if (!string.IsNullOrEmpty(rule.RuleCriteria?.Name) && !rule.RuleCriteria.Disabled) { - var criteriaContext = new RuleCriteriaContext - { - Text = text, - Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) - }; - var criteriaResult = await ExecuteCriteriaAsync(agent, trigger, rule.RuleCriteria?.Name, criteriaContext); - if (criteriaResult == null || !criteriaResult.IsValid) + var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states); + if (criteriaResult?.IsValid == false) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; @@ -62,14 +57,8 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - var actionContext = new RuleActionContext - { - Text = text, - Parameters = BuildContextParameters(rule.RuleAction?.Config, states) - }; - - var action = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; - var actionResult = await ExecuteActionAsync(agent, trigger, action, actionContext); + var actionName = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; + var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states); if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { newConversationIds.Add(actionResult.ConversationId); @@ -81,12 +70,16 @@ public async Task> Triggered(IRuleTrigger trigger, string te #region Criteria - private async Task ExecuteCriteriaAsync( + private async Task ExecuteCriteriaAsync( Agent agent, + AgentRule rule, IRuleTrigger trigger, string? criteriaProvider, - RuleCriteriaContext context) + string text, + IEnumerable? states) { + var result = new RuleCriteriaResult(); + try { var criteria = _services.GetServices() @@ -94,9 +87,16 @@ public async Task> Triggered(IRuleTrigger trigger, string te if (criteria == null) { - return null; + return result; } + + var context = new RuleCriteriaContext + { + Text = text, + Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) + }; + _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", criteria.Provider, agent.Id, trigger.Name); @@ -108,7 +108,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute criteria context.Parameters ??= []; - var result = await criteria.ValidateAsync(agent, trigger, context); + result = await criteria.ValidateAsync(agent, trigger, context); foreach (var hook in hooks) { @@ -120,7 +120,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te catch (Exception ex) { _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", criteriaProvider ?? string.Empty, agent.Id); - return null; + return result; } } #endregion @@ -129,9 +129,11 @@ public async Task> Triggered(IRuleTrigger trigger, string te #region Action private async Task ExecuteActionAsync( Agent agent, + AgentRule rule, IRuleTrigger trigger, - string actionName, - RuleActionContext context) + string? actionName, + string text, + IEnumerable? states) { try { @@ -148,6 +150,12 @@ private async Task ExecuteActionAsync( return RuleActionResult.Failed(errorMsg); } + var context = new RuleActionContext + { + Text = text, + Parameters = BuildContextParameters(rule.RuleAction?.Config, states) + }; + _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", action.Name, agent.Id, trigger.Name); @@ -198,7 +206,7 @@ private async Task ExecuteActionAsync( return dict; } - private Dictionary ConvertToDictionary(JsonDocument doc) + private static Dictionary ConvertToDictionary(JsonDocument doc) { var dict = new Dictionary(); diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 0e4cc9dd0..366157418 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -17,6 +17,12 @@ public IEnumerable GetRuleTriggers() }).OrderBy(x => x.TriggerName); } + [HttpGet("/rule/criteria-providers")] + public async Task> GetRuleCriteriaProviders() + { + return _services.GetServices().Select(x => x.Provider).OrderBy(x => x); + } + [HttpGet("/rule/actions")] public async Task> GetRuleActions() { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index a2cfb4e3e..d031f4a96 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using System; using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -9,7 +8,6 @@ public class AgentRuleMongoElement { public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } - public string Criteria { get; set; } = default!; public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } public AgentRuleActionMongoModel? RuleAction { get; set; } @@ -19,7 +17,6 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria, RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria), RuleAction = AgentRuleActionMongoModel.ToMongoModel(rule.RuleAction) }; @@ -31,7 +28,6 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria, RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria), RuleAction = AgentRuleActionMongoModel.ToDomainModel(rule.RuleAction) }; @@ -41,6 +37,8 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) [BsonIgnoreExtraElements(Inherited = true)] public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel { + public string CriteriaText { get; set; } + public static AgentRuleCriteriaMongoModel? ToMongoModel(AgentRuleCriteria? criteria) { if (criteria == null) @@ -51,6 +49,7 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel return new AgentRuleCriteriaMongoModel { Name = criteria.Name, + CriteriaText = criteria.CriteriaText, Disabled = criteria.Disabled, Config = criteria.Config != null ? BsonDocument.Parse(criteria.Config.RootElement.GetRawText()) : null }; @@ -66,6 +65,7 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel return new AgentRuleCriteria { Name = criteria.Name, + CriteriaText = criteria.CriteriaText, Disabled = criteria.Disabled, Config = criteria.Config != null ? JsonDocument.Parse(criteria.Config.ToJson()) : null }; From 953854a9f317c544dd0d3396c5d0fde512fa178d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 28 Jan 2026 16:10:18 -0600 Subject: [PATCH 32/36] add json options --- .../Rules/Models/RuleActionContext.cs | 3 +++ .../Rules/Models/RuleCriteriaContext.cs | 3 +++ .../Rules/Options/RuleTriggerOptions.cs | 6 ++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 16 ++++++++++------ .../Services/Actions/FunctionCallRuleAction.cs | 2 +- .../Services/Actions/HttpRuleAction.cs | 12 ++++++++++-- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index ca9c6045c..534130c63 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -1,7 +1,10 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs index d777db1d2..709e7be3e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -1,7 +1,10 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Rules.Models; public class RuleCriteriaContext { public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index a2ab78c85..abba98115 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Repositories.Filters; +using System.Text.Json; namespace BotSharp.Abstraction.Rules.Options; @@ -8,4 +9,9 @@ public class RuleTriggerOptions /// Filter agents /// public AgentFilter? AgentFilter { get; set; } + + /// + /// Json serializer options + /// + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index a078f3542..c72ad21bc 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -43,7 +43,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Criteria validation if (!string.IsNullOrEmpty(rule.RuleCriteria?.Name) && !rule.RuleCriteria.Disabled) { - var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states); + var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states, options); if (criteriaResult?.IsValid == false) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); @@ -58,7 +58,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var actionName = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; - var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states); + var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states, options); if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { newConversationIds.Add(actionResult.ConversationId); @@ -76,7 +76,8 @@ private async Task ExecuteCriteriaAsync( IRuleTrigger trigger, string? criteriaProvider, string text, - IEnumerable? states) + IEnumerable? states, + RuleTriggerOptions? triggerOptions) { var result = new RuleCriteriaResult(); @@ -94,7 +95,8 @@ private async Task ExecuteCriteriaAsync( var context = new RuleCriteriaContext { Text = text, - Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) + Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states), + JsonOptions = triggerOptions?.JsonOptions }; _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", @@ -133,7 +135,8 @@ private async Task ExecuteActionAsync( IRuleTrigger trigger, string? actionName, string text, - IEnumerable? states) + IEnumerable? states, + RuleTriggerOptions? triggerOptions) { try { @@ -153,7 +156,8 @@ private async Task ExecuteActionAsync( var context = new RuleActionContext { Text = text, - Parameters = BuildContextParameters(rule.RuleAction?.Config, states) + Parameters = BuildContextParameters(rule.RuleAction?.Config, states), + JsonOptions = triggerOptions?.JsonOptions }; _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs index 6e01f71f4..811db8124 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs @@ -2,7 +2,7 @@ namespace BotSharp.Core.Rules.Services.Actions; -public class FunctionCallRuleAction : IRuleAction +public sealed class FunctionCallRuleAction : IRuleAction { private readonly IServiceProvider _services; private readonly ILogger _logger; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs index f1d2163ce..5e60e6c3f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs @@ -1,6 +1,6 @@ -using BotSharp.Abstraction.Options; using System.Net.Mime; using System.Text.Json; +using System.Text.Json.Serialization; using System.Web; namespace BotSharp.Core.Rules.Services.Actions; @@ -11,6 +11,14 @@ public sealed class HttpRuleAction : IRuleAction private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; + private readonly JsonSerializerOptions _defaultJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + public HttpRuleAction( IServiceProvider services, ILogger logger, @@ -180,7 +188,7 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) return null; } - return JsonSerializer.Serialize(body, BotSharpOptions.defaultJsonOptions); + return JsonSerializer.Serialize(body, context.JsonOptions ?? _defaultJsonOptions); } } From 208b775b55e334dcefdd92c2637841acf9728230 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 29 Jan 2026 09:46:31 -0600 Subject: [PATCH 33/36] validate nullable rule action name --- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index c72ad21bc..bc6297e64 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -54,10 +54,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute action if (rule.RuleAction?.Disabled == true) { - continue; + continue; } - var actionName = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; + var actionName = !string.IsNullOrEmpty(rule?.RuleAction?.Name) ? rule.RuleAction.Name : RuleConstant.DEFAULT_ACTION_NAME; var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states, options); if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { From 2db1aa3963991280804186cee2d7770d6ac4a74c Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 29 Jan 2026 19:15:35 -0600 Subject: [PATCH 34/36] fix namespace and correct unit conversion --- .../Infrastructures/MessageQueues/IMQConsumer.cs | 2 -- .../Infrastructures/MessageQueues/MQConsumerBase.cs | 3 +-- .../Services/Actions/MessageQueueRuleAction.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs index 2f9296689..4df43dd0e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; - namespace BotSharp.Abstraction.Infrastructures.MessageQueues; /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index 2fda58581..cd66be1fd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -1,7 +1,6 @@ -using BotSharp.Abstraction.Infrastructures.MessageQueues; using Microsoft.Extensions.Logging; -namespace BotSharp.Plugin.RabbitMQ.Consumers; +namespace BotSharp.Abstraction.Infrastructures.MessageQueues; /// /// Abstract base class for RabbitMQ consumers. diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs index fcd406b55..1ee75b09e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs @@ -111,7 +111,7 @@ private long ParseDelay(RuleActionContext context) break; case "minute": case "minutes": - milliseconds = (long)TimeSpan.FromMilliseconds(qty).TotalMilliseconds; + milliseconds = (long)TimeSpan.FromMinutes(qty).TotalMilliseconds; break; case "hour": case "hours": From 30c670a32f8bf2af47faea1f5b50a740cc100b6b Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 29 Jan 2026 19:33:06 -0600 Subject: [PATCH 35/36] refine type conversion and mq channel --- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 11 +++-------- .../Services/RabbitMQService.cs | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index bc6297e64..53b748ea5 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -219,19 +219,14 @@ private async Task ExecuteActionAsync( dict[prop.Name] = prop.Value.ValueKind switch { JsonValueKind.String => prop.Value.GetString(), + JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, + JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, JsonValueKind.Number when prop.Value.TryGetInt32(out int intValue) => intValue, JsonValueKind.Number when prop.Value.TryGetInt64(out long longValue) => longValue, - JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, - JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, - JsonValueKind.Number when prop.Value.TryGetByte(out byte byteValue) => byteValue, - JsonValueKind.Number when prop.Value.TryGetSByte(out sbyte sbyteValue) => sbyteValue, - JsonValueKind.Number when prop.Value.TryGetUInt16(out ushort uint16Value) => uint16Value, - JsonValueKind.Number when prop.Value.TryGetUInt32(out uint uint32Value) => uint32Value, - JsonValueKind.Number when prop.Value.TryGetUInt64(out ulong uint64Value) => uint64Value, JsonValueKind.Number when prop.Value.TryGetDateTime(out DateTime dateTimeValue) => dateTimeValue, JsonValueKind.Number when prop.Value.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffsetValue) => dateTimeOffsetValue, JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue, - JsonValueKind.Number => prop.Value.GetRawText(), + JsonValueKind.Number => prop.Value.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 0ea2465a7..56774e885 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -155,7 +155,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); var isHandled = await registration.Consumer.HandleMessageAsync(config.QueueName, data); - if (!config.AutoAck && registration.Channel != null) + if (!config.AutoAck && registration.Channel?.IsOpen == true) { if (isHandled) { @@ -170,7 +170,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel catch (Exception ex) { _logger.LogError(ex, $"Error consuming message on queue '{config.QueueName}': {data}"); - if (!config.AutoAck && registration.Channel != null) + if (!config.AutoAck && registration.Channel?.IsOpen == true) { await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); } From 17b93b18f56fe4bdd969adf3ac67d056d7a96df0 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 29 Jan 2026 21:58:32 -0600 Subject: [PATCH 36/36] add json options in mq publish options --- .../MessageQueues/Models/MQPublishOptions.cs | 9 ++++++++- .../Services/Actions/MessageQueueRuleAction.cs | 7 ++++--- .../Models/RabbitMQConsumerConfig.cs | 5 ----- .../Services/RabbitMQService.cs | 12 ++++++------ 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs index b7c31d20e..dead523be 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; /// @@ -29,5 +31,10 @@ public class MQPublishOptions /// /// Additional arguments for the publish configuration (MQ-specific). /// - public Dictionary Arguments { get; set; } = new(); + public Dictionary Arguments { get; set; } = []; + + /// + /// Json serializer options + /// + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs index 1ee75b09e..2f819c6f5 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs @@ -45,7 +45,7 @@ public async Task ExecuteAsync( }; // Publish message to queue - var mqOptions = GetMQPublishOptions(context); + var mqOptions = BuildMQPublishOptions(context); var success = await mqService.PublishAsync(payload, mqOptions); if (success) @@ -72,7 +72,7 @@ public async Task ExecuteAsync( } } - private MQPublishOptions GetMQPublishOptions(RuleActionContext context) + private MQPublishOptions BuildMQPublishOptions(RuleActionContext context) { var topicName = context.Parameters.TryGetValueOrDefault("mq_topic_name", string.Empty); var routingKey = context.Parameters.TryGetValueOrDefault("mq_routing_key", string.Empty); @@ -82,7 +82,8 @@ private MQPublishOptions GetMQPublishOptions(RuleActionContext context) { TopicName = topicName, RoutingKey = routingKey, - DelayMilliseconds = delayMilliseconds + DelayMilliseconds = delayMilliseconds, + JsonOptions = context.JsonOptions }; } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs index 4dc8f8ed5..93754d455 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs @@ -17,11 +17,6 @@ internal class RabbitMQConsumerConfig /// internal string RoutingKey { get; set; } = "rabbitmq.routing"; - /// - /// Whether to automatically acknowledge messages. - /// - internal bool AutoAck { get; set; } = false; - /// /// Additional arguments for the consumer configuration. /// diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 56774e885..0117bad14 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -86,7 +86,7 @@ public async Task UnsubscribeAsync(string key) await channel.BasicConsumeAsync( queue: config.QueueName, - autoAck: config.AutoAck, + autoAck: false, consumer: asyncConsumer); _logger.LogWarning($"RabbitMQ consuming queue '{config.QueueName}'."); @@ -155,7 +155,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); var isHandled = await registration.Consumer.HandleMessageAsync(config.QueueName, data); - if (!config.AutoAck && registration.Channel?.IsOpen == true) + if (registration.Channel?.IsOpen == true) { if (isHandled) { @@ -170,7 +170,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel catch (Exception ex) { _logger.LogError(ex, $"Error consuming message on queue '{config.QueueName}': {data}"); - if (!config.AutoAck && registration.Channel?.IsOpen == true) + if (registration.Channel?.IsOpen == true) { await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); } @@ -222,7 +222,7 @@ await channel.ExchangeDeclareAsync( var messageId = options.MessageId ?? Guid.NewGuid().ToString(); var message = new MQMessage(payload, messageId); - var body = ConvertToBinary(message); + var body = ConvertToBinary(message, options.JsonOptions); var properties = new BasicProperties { MessageId = messageId, @@ -272,9 +272,9 @@ private RetryPolicy BuildRetryPolicy() }); } - private byte[] ConvertToBinary(T data) + private static byte[] ConvertToBinary(T data, JsonSerializerOptions? jsonOptions = null) { - var jsonStr = JsonSerializer.Serialize(data); + var jsonStr = JsonSerializer.Serialize(data, jsonOptions); var body = Encoding.UTF8.GetBytes(jsonStr); return body; }