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