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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] 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/91] refine action and add mq channel pool --- .../Agents/Models/AgentRule.cs | 34 ------ .../Rules/Enums/RuleActionType.cs | 9 -- .../Rules/Enums/RuleDelayUnit.cs | 9 -- .../Rules/Hooks/IRuleTriggerHook.cs | 14 +++ .../Rules/Hooks/RuleTriggerHookBase.cs | 5 + .../BotSharp.Abstraction/Rules/IRuleAction.cs | 35 +++--- .../Rules/IRuleCriteria.cs | 4 +- ...tActionPayload.cs => RuleActionContext.cs} | 3 +- .../Rules/Models/RuleActionResult.cs | 46 ++++++++ .../Rules/Models/RuleHttpContext.cs | 13 +++ .../Rules/Models/RuleHttpResult.cs | 6 ++ .../Rules/Options/RuleActionOptions.cs | 19 ---- .../Rules/Options/RuleEventMessageOptions.cs | 34 ------ .../Rules/Options/RuleMethodOptions.cs | 6 -- .../Rules/Options/RuleTriggerOptions.cs | 4 +- .../Constants/RuleHandler.cs | 6 -- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 88 ++++++++------- .../BotSharp.Core.Rules/RulesPlugin.cs | 4 +- .../Services/ChatRuleAction.cs | 68 ++++++++++++ .../Services/RuleAction.Chat.cs | 44 -------- .../Services/RuleAction.Event.cs | 38 ------- .../Services/RuleAction.Http.cs | 9 -- .../Services/RuleAction.Method.cs | 18 ---- .../Services/RuleAction.cs | 17 --- .../Services/RuleCriteria.cs | 8 +- .../BotSharp.Core.Rules/Using.cs | 3 - src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs | 2 +- .../Connections/RabbitMQChannelPool.cs | 73 +++++++++++++ .../Connections/RabbitMQChannelPoolFactory.cs | 13 +++ .../Connections/RabbitMQConnection.cs | 2 + .../Interfaces/IRabbitMQConnection.cs | 1 + .../RabbitMQPlugin.cs | 2 +- .../Services/RabbitMQService.cs | 102 +++++++++++------- src/WebStarter/appsettings.json | 2 +- 34 files changed, 390 insertions(+), 351 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs rename src/Infrastructure/BotSharp.Abstraction/Rules/Models/{RuleChatActionPayload.cs => RuleActionContext.cs} (66%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index e156cfda8..dfe03681d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Rules.Enums; - namespace BotSharp.Abstraction.Agents.Models; public class AgentRule @@ -15,36 +13,4 @@ public class AgentRule [JsonPropertyName("action")] public string? Action { get; set; } - - [JsonPropertyName("delay")] - public RuleDelay? Delay { get; set; } -} - -public class RuleDelay -{ - public int Quantity { get; set; } - public string Unit { get; set; } - - public TimeSpan? Parse() - { - TimeSpan? ts = null; - - switch (Unit) - { - case RuleDelayUnit.Second: - ts = TimeSpan.FromSeconds(Quantity); - break; - case RuleDelayUnit.Minute: - ts = TimeSpan.FromMinutes(Quantity); - break; - case RuleDelayUnit.Hour: - ts = TimeSpan.FromHours(Quantity); - break; - case RuleDelayUnit.Day: - ts = TimeSpan.FromDays(Quantity); - break; - } - - return ts; - } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs deleted file mode 100644 index acb05e417..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleActionType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Enums; - -public static class RuleActionType -{ - public const string Chat = "chat"; - public const string Http = "http"; - public const string EventMessage = "event-message"; - public const string Method = "method"; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs deleted file mode 100644 index cc862e68b..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Enums/RuleDelayUnit.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Enums; - -public static class RuleDelayUnit -{ - public const string Second = "second"; - public const string Minute = "minute"; - public const string Hour = "hour"; - public const string Day = "day"; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs new file mode 100644 index 000000000..bfeda4086 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -0,0 +1,14 @@ +using BotSharp.Abstraction.Hooks; +using BotSharp.Abstraction.Instructs.Models; +using BotSharp.Abstraction.Rules.Models; + +namespace BotSharp.Abstraction.Rules.Hooks; + +public interface IRuleTriggerHook : IHookBase +{ + Task BeforeSendEventMessage(Agent agent, RoleDialogModel message) => Task.CompletedTask; + Task AfterSendEventMessage(Agent agent, InstructResult result) => Task.CompletedTask; + + Task BeforeSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpContext message) => Task.CompletedTask; + Task AfterSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpResult result) => Task.CompletedTask; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs new file mode 100644 index 000000000..60bdf7cf5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs @@ -0,0 +1,5 @@ +namespace BotSharp.Abstraction.Rules.Hooks; + +public class RuleTriggerHookBase : IRuleTriggerHook +{ +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 70306a1a5..9c2bf03d9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -1,20 +1,29 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Rules.Models; +using BotSharp.Abstraction.Rules.Options; namespace BotSharp.Abstraction.Rules; +/// +/// Base interface for rule actions that can be executed by the RuleEngine +/// public interface IRuleAction { - string Provider { get; } + /// + /// The unique name of the rule action provider + /// + string Name { get; } - Task SendChatAsync(Agent agent, RuleChatActionPayload payload) - => throw new NotImplementedException(); - - Task SendHttpRequestAsync() - => throw new NotImplementedException(); - - Task SendEventMessageAsync(RuleDelay delay, RuleEventMessageOptions? options) - => throw new NotImplementedException(); - - Task ExecuteMethodAsync(Agent agent, Func func) - => throw new NotImplementedException(); -} + /// + /// Execute the rule action + /// + /// The agent that triggered the rule + /// The rule trigger + /// The action context + /// The action execution result + Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs index 600ebb546..af5d5cf3d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs @@ -4,6 +4,6 @@ public interface IRuleCriteria { string Provider { get; } - Task ExecuteCriteriaAsync(Agent agent, string triggerName, CriteriaExecuteOptions options) - => throw new NotImplementedException(); + Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) + => Task.FromResult(false); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs similarity index 66% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 84c22353a..d6a0d5570 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleChatActionPayload.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -1,8 +1,7 @@ namespace BotSharp.Abstraction.Rules.Models; -public class RuleChatActionPayload +public class RuleActionContext { public string Text { get; set; } - public string Channel { get; set; } public IEnumerable? States { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs new file mode 100644 index 000000000..ffdaeab2e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs @@ -0,0 +1,46 @@ +namespace BotSharp.Abstraction.Rules.Models; + +/// +/// Result of a rule action execution +/// +public class RuleActionResult +{ + /// + /// Whether the action executed successfully + /// + public bool Success { get; set; } + + /// + /// The conversation ID if a new conversation was created + /// + public string? ConversationId { get; set; } + + /// + /// Response content from the action + /// + public string? Response { get; set; } + + /// + /// Error message if the action failed + /// + public string? ErrorMessage { get; set; } + + public static RuleActionResult Succeeded(string? response = null) + { + return new RuleActionResult + { + Success = true, + Response = response + }; + } + + public static RuleActionResult Failed(string errorMessage) + { + return new RuleActionResult + { + Success = false, + ErrorMessage = errorMessage + }; + } +} + diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs new file mode 100644 index 000000000..0268d12a1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs @@ -0,0 +1,13 @@ +using System.Net.Http; + +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleHttpContext +{ + public string BaseUrl { get; set; } + public string RelativeUrl { get; set; } + public HttpMethod Method { get; set; } + public Dictionary Headers { get; set; } = []; + public Dictionary QueryParams { get; set; } = []; + public string RequestBody { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs new file mode 100644 index 000000000..c5109a5c7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleHttpResult +{ + public string HttpResponse { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs deleted file mode 100644 index a17bb2584..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleActionOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleActionOptions -{ - /// - /// Rule action provider - /// - public string Provider { get; set; } = "botsharp-rule"; - - /// - /// Event message options - /// - public RuleEventMessageOptions? EventMessage { get; set; } - - /// - /// Custom method options - /// - public RuleMethodOptions? Method { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs deleted file mode 100644 index 5fe228dba..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleEventMessageOptions.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleEventMessageOptions -{ - /// - /// Message payload - /// - public string Payload { get; set; } - - /// - /// Exchange - /// - public string Exchange { get; set; } - - /// - /// Routing key - /// - public string RoutingKey { get; set; } - - /// - /// Delayed message id - /// - public string? MessageId { get; set; } - - /// - /// Arguments - /// - public Dictionary Arguments { get; set; } = new(); - - public override string ToString() - { - return $"{Exchange}-{RoutingKey} => {Payload}"; - } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs deleted file mode 100644 index 424efda08..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleMethodOptions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleMethodOptions -{ - public Func? Func { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 1eb07c86c..93703d98f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -2,7 +2,9 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleTriggerOptions { + /// + /// Criteria options for validating whether the rule should be triggered + /// public RuleCriteriaOptions? Criteria { get; set; } - public RuleActionOptions? Action { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs deleted file mode 100644 index 9c35a4139..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleHandler.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Core.Rules.Constants; - -public static class RuleHandler -{ - public const string DefaultProvider = "botsharp-rule"; -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 69d2d8fa9..6fa72b145 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,5 +1,3 @@ -using System.Data; - namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -33,69 +31,77 @@ public async Task> Triggered(IRuleTrigger trigger, string te var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); foreach (var agent in filteredAgents) { - // Criteria + // Criteria validation if (options?.Criteria != null) { var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? RuleHandler.DefaultProvider)); + .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "botsharp-rule-criteria")); if (criteria == null) { + _logger.LogWarning("No criteria provider found for {Provider}, skipping agent {AgentId}", options.Criteria.Provider, agent.Id); continue; } - var isTriggered = await criteria.ExecuteCriteriaAsync(agent, trigger.Name, options.Criteria); - if (!isTriggered) + var isValid = await criteria.ValidateAsync(agent, trigger, options.Criteria); + if (!isValid) { + _logger.LogDebug("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; } } - var foundTrigger = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundTrigger == null) + var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); + if (foundRule == null) { continue; } - var action = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Action?.Provider ?? RuleHandler.DefaultProvider)); - if (action == null) + var context = new RuleActionContext { - continue; - } - - // Execute action - if (foundTrigger.Action.IsEqualTo(RuleActionType.Method)) + Text = text, + States = states + }; + var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("Chat")!, context); + if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { - if (options?.Action?.Method?.Func != null) - { - await action.ExecuteMethodAsync(agent, options.Action.Method.Func); - } + newConversationIds.Add(result.ConversationId); } - else if (foundTrigger.Action.IsEqualTo(RuleActionType.EventMessage)) - { - await action.SendEventMessageAsync(foundTrigger.Delay, options?.Action?.EventMessage); - } - else if (foundTrigger.Action.IsEqualTo(RuleActionType.Http)) + } + + return newConversationIds; + } + + private async Task ExecuteActionAsync( + Agent agent, + IRuleTrigger trigger, + string actionName, + RuleActionContext context) + { + try + { + // Get all registered rule actions + var actions = _services.GetServices(); + + // Find the matching action + var action = actions.FirstOrDefault(x => x.Name.IsEqualTo(actionName)); + + if (action == null) { - + var errorMsg = $"No rule action {actionName} is found"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); } - else - { - var conversationId = await action.SendChatAsync(agent, payload: new() - { - Text = text, - Channel = trigger.Channel, - States = states - }); - if (!string.IsNullOrEmpty(conversationId)) - { - newConversationIds.Add(conversationId); - } - } - } + _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", + action.Name, agent.Id, trigger.Name); - return newConversationIds; + return await action.ExecuteAsync(agent, trigger, context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", actionName, agent.Id); + return RuleActionResult.Failed(ex.Message); + } } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 6135da50d..aee5a7fc7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -19,6 +19,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) { services.AddScoped(); services.AddScoped(); - services.AddScoped(); + + // Register rule actions + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs new file mode 100644 index 000000000..903bcdb00 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs @@ -0,0 +1,68 @@ +namespace BotSharp.Core.Rules.Services; + +public sealed class ChatRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public ChatRuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Name => "Chat"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + var channel = trigger.Channel; + var convService = _services.GetRequiredService(); + var conv = await convService.NewConversation(new Conversation + { + Channel = channel, + Title = context.Text, + AgentId = agent.Id + }); + + var message = new RoleDialogModel(AgentRole.User, context.Text); + + var allStates = new List + { + new("channel", channel) + }; + + if (!context.States.IsNullOrEmpty()) + { + allStates.AddRange(context.States!); + } + + await convService.SetConversationId(conv.Id, allStates); + await convService.SendMessage(agent.Id, + message, + null, + msg => Task.CompletedTask); + + await convService.SaveStates(); + + _logger.LogInformation("Chat rule action executed successfully for agent {AgentId}, conversation {ConversationId}", agent.Id, conv.Id); + + return new RuleActionResult + { + Success = true, + ConversationId = conv.Id + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error when sending chat via rule action for agent {AgentId} and trigger {TriggerName}", agent.Id, trigger.Name); + return RuleActionResult.Failed(ex.Message); + } + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs deleted file mode 100644 index 84925a92d..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Chat.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction : IRuleAction -{ - public async Task SendChatAsync(Agent agent, RuleChatActionPayload payload) - { - try - { - var convService = _services.GetRequiredService(); - var conv = await convService.NewConversation(new Conversation - { - Channel = payload.Channel, - Title = payload.Text, - AgentId = agent.Id - }); - - var message = new RoleDialogModel(AgentRole.User, payload.Text); - - var allStates = new List - { - new("channel", payload.Channel) - }; - - if (!payload.States.IsNullOrEmpty()) - { - allStates.AddRange(payload.States!); - } - - await convService.SetConversationId(conv.Id, allStates); - await convService.SendMessage(agent.Id, - message, - null, - msg => Task.CompletedTask); - - await convService.SaveStates(); - return conv.Id; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when sending chat via rule action."); - return string.Empty; - } - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs deleted file mode 100644 index 50db54fd1..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Event.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction -{ - public async Task SendEventMessageAsync(RuleDelay delay, RuleEventMessageOptions? options) - { - if (options == null || delay == null || delay.Quantity < 0) - { - return false; - } - - var mqService = _services.GetService(); - if (mqService == null) - { - return false; - } - - var ts = delay.Parse(); - if (!ts.HasValue) - { - return false; - } - - _logger.LogWarning($"Start sending delay message {options}"); - - var isSent = await mqService.PublishAsync(options.Payload, options: new() - { - Exchange = options.Exchange, - RoutingKey = options.RoutingKey, - MessageId = options.MessageId, - MilliSeconds = (long)ts.Value.TotalMilliseconds, - Arguments = options.Arguments - }); - - _logger.LogWarning($"Complete sending delay message: {(isSent ? "Success" : "Failed")}"); - return isSent; - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs deleted file mode 100644 index 01590a3be..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Http.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction -{ - public Task SendHttpRequestAsync() - { - throw new NotImplementedException(); - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs deleted file mode 100644 index b036ffc10..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.Method.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction -{ - public async Task ExecuteMethodAsync(Agent agent, Func func) - { - try - { - await func(agent); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when executing custom method."); - return false; - } - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs deleted file mode 100644 index 0fd271550..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleAction.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace BotSharp.Core.Rules.Services; - -public partial class RuleAction : IRuleAction -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - - public RuleAction( - IServiceProvider services, - ILogger logger) - { - _services = services; - _logger = logger; - } - - public string Provider => RuleHandler.DefaultProvider; -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs index 2ae06253d..015c6e910 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs @@ -18,9 +18,9 @@ public RuleCriteria( _codingSettings = codingSettings; } - public string Provider => RuleHandler.DefaultProvider; + public string Provider => "botsharp-rule-criteria"; - public async Task ExecuteCriteriaAsync(Agent agent, string triggerName, CriteriaExecuteOptions options) + public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) { if (string.IsNullOrWhiteSpace(agent?.Id)) { @@ -36,10 +36,10 @@ public async Task ExecuteCriteriaAsync(Agent agent, string triggerName, Cr } var agentService = _services.GetRequiredService(); - var scriptName = options.CodeScriptName ?? $"{triggerName}_rule.py"; + var scriptName = options.CodeScriptName ?? $"{trigger.Name}_rule.py"; var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - var msg = $"rule trigger ({triggerName}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; + var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index 0f999ff88..2cfb617d2 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -16,7 +16,6 @@ global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Repositories.Filters; global using BotSharp.Abstraction.Rules; -global using BotSharp.Abstraction.Rules.Enums; global using BotSharp.Abstraction.Rules.Options; global using BotSharp.Abstraction.Rules.Models; global using BotSharp.Abstraction.Utilities; @@ -27,5 +26,3 @@ global using BotSharp.Abstraction.Coding.Utils; global using BotSharp.Abstraction.Coding.Settings; global using BotSharp.Abstraction.Hooks; - -global using BotSharp.Core.Rules.Constants; diff --git a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs index be189898e..8e29bb1bb 100644 --- a/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs +++ b/src/Plugins/BotSharp.Plugin.Graph/GraphDb.cs @@ -84,7 +84,7 @@ private async Task SendRequest(string url, GraphQueryRequest r } catch (Exception ex) { - _logger.LogError(ex, $"Error when fetching Lessen GLM response (Endpoint: {url})."); + _logger.LogError(ex, $"Error when fetching {Provider} Graph db response (Endpoint: {url})."); return result; } } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs new file mode 100644 index 000000000..81c7de270 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPool.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.ObjectPool; +using RabbitMQ.Client; + +namespace BotSharp.Plugin.RabbitMQ.Connections; + +public class RabbitMQChannelPool +{ + private readonly ObjectPool _pool; + private readonly ILogger _logger; + private readonly int _tryLimit = 3; + + public RabbitMQChannelPool( + IServiceProvider services, + IRabbitMQConnection mqConnection) + { + _logger = services.GetRequiredService().CreateLogger(); + var poolProvider = new DefaultObjectPoolProvider(); + var policy = new ChannelPoolPolicy(mqConnection.Connection); + _pool = poolProvider.Create(policy); + } + + public IChannel Get() + { + var count = 0; + var channel = _pool.Get(); + + while (count < _tryLimit && channel.IsClosed) + { + channel.Dispose(); + channel = _pool.Get(); + count++; + } + + if (channel.IsClosed) + { + _logger.LogWarning($"No open channel from the pool after {_tryLimit} retries."); + } + + return channel; + } + + public void Return(IChannel channel) + { + if (channel.IsOpen) + { + _pool.Return(channel); + } + else + { + channel.Dispose(); + } + } +} + +internal class ChannelPoolPolicy : IPooledObjectPolicy +{ + private readonly IConnection _connection; + + public ChannelPoolPolicy(IConnection connection) + { + _connection = connection; + } + + public IChannel Create() + { + return _connection.CreateChannelAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public bool Return(IChannel obj) + { + return true; + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs new file mode 100644 index 000000000..989c0a7b7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQChannelPoolFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Concurrent; + +namespace BotSharp.Plugin.RabbitMQ.Connections; + +public static class RabbitMQChannelPoolFactory +{ + private static readonly ConcurrentDictionary _poolDict = new(); + + public static RabbitMQChannelPool GetChannelPool(IServiceProvider services, IRabbitMQConnection rabbitMQConnection) + { + return _poolDict.GetOrAdd(rabbitMQConnection.Connection.ToString()!, key => new RabbitMQChannelPool(services, rabbitMQConnection)); + } +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs index c2782e9fe..dac9e8c07 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Connections/RabbitMQConnection.cs @@ -38,6 +38,8 @@ public RabbitMQConnection( public bool IsConnected => _connection != null && _connection.IsOpen && !_disposed; + public IConnection Connection => _connection; + public async Task CreateChannelAsync() { if (!IsConnected) diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs index c5266d630..cb89c2976 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Interfaces/IRabbitMQConnection.cs @@ -5,6 +5,7 @@ namespace BotSharp.Plugin.RabbitMQ.Interfaces; public interface IRabbitMQConnection : IDisposable { bool IsConnected { get; } + IConnection Connection { get; } Task CreateChannelAsync(); Task ConnectAsync(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index 3a841fcd1..d1ddc75c3 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -16,7 +16,7 @@ public class RabbitMQPlugin : IBotSharpAppPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { var settings = new RabbitMQSettings(); - config.Bind("RabbitMQ", settings); + config.Bind("RabbitMessageQueue", settings); services.AddSingleton(settings); var mqSettings = new MessageQueueSettings(); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 27ef23e60..529660abe 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -1,3 +1,4 @@ +using BotSharp.Plugin.RabbitMQ.Connections; using Polly; using Polly.Retry; using RabbitMQ.Client; @@ -9,6 +10,7 @@ namespace BotSharp.Plugin.RabbitMQ.Services; public class RabbitMQService : IMQService { private readonly IRabbitMQConnection _mqConnection; + private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly int _retryCount = 5; @@ -17,9 +19,11 @@ public class RabbitMQService : IMQService public RabbitMQService( IRabbitMQConnection mqConnection, + IServiceProvider services, ILogger logger) { _mqConnection = mqConnection; + _services = services; _logger = logger; } @@ -150,11 +154,17 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel data = Encoding.UTF8.GetString(eventArgs.Body.Span); _logger.LogInformation($"Message received on '{options.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); - await registration.Consumer.HandleMessageAsync(options.QueueName, data); - + var isDone = await registration.Consumer.HandleMessageAsync(options.QueueName, data); if (!options.AutoAck && registration.Channel != null) { - await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); + if (isDone) + { + await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); + } + else + { + await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); + } } } catch (Exception ex) @@ -176,52 +186,68 @@ public async Task PublishAsync(T payload, MQPublishOptions options) await _mqConnection.ConnectAsync(); } + var isPublished = false; var policy = BuildRetryPolicy(); await policy.Execute(async () => { - await using var channel = await _mqConnection.CreateChannelAsync(); - var args = new Dictionary - { - ["x-delayed-type"] = "direct" - }; + var channelPool = RabbitMQChannelPoolFactory.GetChannelPool(_services, _mqConnection); + var channel = channelPool.Get(); - if (options.Arguments != null) + try { - foreach (var kvp in options.Arguments) + var args = new Dictionary { - args[kvp.Key] = kvp.Value; - } - } + ["x-delayed-type"] = "direct" + }; - await channel.ExchangeDeclareAsync( - exchange: options.Exchange, - type: "x-delayed-message", - durable: true, - autoDelete: false, - arguments: args); - - var messageId = options.MessageId ?? Guid.NewGuid().ToString(); - var message = new MQMessage(payload, messageId); - var body = ConvertToBinary(message); - var properties = new BasicProperties - { - MessageId = messageId, - DeliveryMode = DeliveryModes.Persistent, - Headers = new Dictionary + if (options.Arguments != null) { - ["x-delay"] = options.MilliSeconds + foreach (var kvp in options.Arguments) + { + args[kvp.Key] = kvp.Value; + } } - }; - - await channel.BasicPublishAsync( - exchange: options.Exchange, - routingKey: options.RoutingKey, - mandatory: true, - basicProperties: properties, - body: body); + + await channel.ExchangeDeclareAsync( + exchange: options.Exchange, + type: "x-delayed-message", + durable: true, + autoDelete: false, + arguments: args); + + var messageId = options.MessageId ?? Guid.NewGuid().ToString(); + var message = new MQMessage(payload, messageId); + var body = ConvertToBinary(message); + var properties = new BasicProperties + { + MessageId = messageId, + DeliveryMode = DeliveryModes.Persistent, + Headers = new Dictionary + { + ["x-delay"] = options.MilliSeconds + } + }; + + await channel.BasicPublishAsync( + exchange: options.Exchange, + routingKey: options.RoutingKey, + mandatory: true, + basicProperties: properties, + body: body); + + isPublished = true; + } + catch (Exception) + { + throw; + } + finally + { + channelPool.Return(channel); + } }); - return true; + return isPublished; } catch (Exception ex) { diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 322315192..62e55d45f 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1025,7 +1025,7 @@ "Provider": "RabbitMQ" }, - "RabbitMQ": { + "RabbitMessageQueue": { "HostName": "localhost", "Port": 5672, "UserName": "guest", From ee2e444193cdad250333d7349dd9ef7a94518d73 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 22 Jan 2026 20:09:18 -0600 Subject: [PATCH 19/91] refine config --- .../MessageQueues/IMQConsumer.cs | 4 +- .../MessageQueues/MQConsumerBase.cs | 4 +- ...ConsumerOptions.cs => MQConsumerConfig.cs} | 2 +- .../MessageQueues/Models/MQPublishOptions.cs | 29 +++++++++-- .../Consumers/DummyMessageConsumer.cs | 2 +- .../Consumers/ScheduledMessageConsumer.cs | 2 +- .../Controllers/RabbitMQController.cs | 4 +- .../Models/RabbitMQConsumerConfig.cs | 29 +++++++++++ .../RabbitMQPlugin.cs | 2 - .../Services/RabbitMQService.cs | 48 +++++++++---------- src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs | 3 +- 11 files changed, 90 insertions(+), 39 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/{MQConsumerOptions.cs => MQConsumerConfig.cs} (97%) create mode 100644 src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs index f0aff8c1b..25fcda5a2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -9,9 +9,9 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues; public interface IMQConsumer : IDisposable { /// - /// Gets the consumer options containing exchange, queue and routing configuration. + /// Gets the consumer config /// - MQConsumerOptions Options { get; } + object Config { get; } /// /// Handles the received message from the queue. diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index e38c26d9b..73b7572bc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -16,10 +16,10 @@ public abstract class MQConsumerBase : IMQConsumer private bool _disposed = false; /// - /// Gets the consumer options for this consumer. + /// Gets the consumer config for this consumer. /// Override this property to customize exchange, queue and routing configuration. /// - public abstract MQConsumerOptions Options { get; } + public abstract object Config { get; } protected MQConsumerBase( IServiceProvider services, diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs similarity index 97% rename from src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs index 7aa9bf02e..bb3072a14 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs @@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; /// Configuration options for message queue consumers. /// These options are MQ-product agnostic and can be adapted by different implementations. /// -public class MQConsumerOptions +public class MQConsumerConfig { /// /// The exchange name (topic in some MQ systems). diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs index a71df3574..b7c31d20e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs @@ -1,10 +1,33 @@ namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; +/// +/// Configuration options for publishing messages to a message queue. +/// These options are MQ-product agnostic and can be adapted by different implementations. +/// public class MQPublishOptions { - public string Exchange { get; set; } - public string RoutingKey { get; set; } - public long MilliSeconds { get; set; } + /// + /// The topic name (exchange in RabbitMQ, topic in Kafka/Azure Service Bus). + /// + public string TopicName { get; set; } = string.Empty; + + /// + /// The routing key (partition key in some MQ systems, used for message routing). + /// + public string RoutingKey { get; set; } = string.Empty; + + /// + /// Delay in milliseconds before the message is delivered. + /// + public long DelayMilliseconds { get; set; } + + /// + /// Optional unique identifier for the message. + /// public string? MessageId { get; set; } + + /// + /// Additional arguments for the publish configuration (MQ-specific). + /// public Dictionary Arguments { get; set; } = new(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs index 4ce9282cb..bdefa3131 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class DummyMessageConsumer : MQConsumerBase { - public override MQConsumerOptions Options => new() + public override MQConsumerConfig Config => new() { ExchangeName = "my.exchange", QueueName = "dummy.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs index b2deb177e..b28089a94 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class ScheduledMessageConsumer : MQConsumerBase { - public override MQConsumerOptions Options => new() + public override object Config => new { ExchangeName = "my.exchange", QueueName = "scheduled.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs index be9a3d834..802e4fa1b 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Controllers/RabbitMQController.cs @@ -45,9 +45,9 @@ public async Task PublishScheduledMessage([FromBody] PublishSched payload, options: new() { - Exchange = "my.exchange", + TopicName = "my.exchange", RoutingKey = "my.routing", - MilliSeconds = request.DelayMilliseconds ?? 10000, + DelayMilliseconds = request.DelayMilliseconds ?? 10000, MessageId = request.MessageId }); return Ok(new { Success = success }); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs new file mode 100644 index 000000000..1790fe1ab --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs @@ -0,0 +1,29 @@ +namespace BotSharp.Plugin.RabbitMQ.Models; + +internal class RabbitMQConsumerConfig +{ + /// + /// The exchange name (topic in some MQ systems). + /// + public string ExchangeName { get; set; } = "rabbitmq.exchange"; + + /// + /// The queue name (subscription in some MQ systems). + /// + public string QueueName { get; set; } = "rabbitmq.queue"; + + /// + /// The routing key (filter in some MQ systems). + /// + public string RoutingKey { get; set; } = "rabbitmq.routing"; + + /// + /// Whether to automatically acknowledge messages. + /// + public bool AutoAck { get; set; } = false; + + /// + /// Additional arguments for the consumer configuration. + /// + public Dictionary Arguments { get; set; } = new(); +} diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index d1ddc75c3..9da987eec 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -1,8 +1,6 @@ -using BotSharp.Plugin.RabbitMQ.Connections; using BotSharp.Plugin.RabbitMQ.Services; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace BotSharp.Plugin.RabbitMQ; diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 529660abe..96c53f1ad 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -1,4 +1,3 @@ -using BotSharp.Plugin.RabbitMQ.Connections; using Polly; using Polly.Retry; using RabbitMQ.Client; @@ -38,7 +37,8 @@ public async Task SubscribeAsync(string key, IMQConsumer consumer) var registration = await CreateConsumerRegistrationAsync(consumer); if (registration != null && _consumers.TryAdd(key, registration)) { - _logger.LogInformation($"Consumer '{key}' subscribed to queue '{consumer.Options.QueueName}'."); + var config = consumer.Config as RabbitMQConsumerConfig ?? new(); + _logger.LogInformation($"Consumer '{key}' subscribed to queue '{config.QueueName}'."); return true; } @@ -75,7 +75,7 @@ public async Task UnsubscribeAsync(string key) { var channel = await CreateChannelAsync(consumer); - var options = consumer.Options; + var config = consumer.Config as RabbitMQConsumerConfig ?? new(); var registration = new ConsumerRegistration(consumer, channel); var asyncConsumer = new AsyncEventingBasicConsumer(channel); @@ -85,11 +85,11 @@ public async Task UnsubscribeAsync(string key) }; await channel.BasicConsumeAsync( - queue: options.QueueName, - autoAck: options.AutoAck, + queue: config.QueueName, + autoAck: config.AutoAck, consumer: asyncConsumer); - _logger.LogWarning($"RabbitMQ consuming queue '{options.QueueName}'."); + _logger.LogWarning($"RabbitMQ consuming queue '{config.QueueName}'."); return registration; } catch (Exception ex) @@ -106,40 +106,40 @@ private async Task CreateChannelAsync(IMQConsumer consumer) await _mqConnection.ConnectAsync(); } - var options = consumer.Options; + var config = consumer.Config as RabbitMQConsumerConfig ?? new(); var channel = await _mqConnection.CreateChannelAsync(); - _logger.LogWarning($"Created RabbitMQ channel {channel.ChannelNumber} for queue '{options.QueueName}'"); + _logger.LogWarning($"Created RabbitMQ channel {channel.ChannelNumber} for queue '{config.QueueName}'"); var args = new Dictionary { ["x-delayed-type"] = "direct" }; - if (options.Arguments != null) + if (config.Arguments != null) { - foreach (var kvp in options.Arguments) + foreach (var kvp in config.Arguments) { args[kvp.Key] = kvp.Value; } } await channel.ExchangeDeclareAsync( - exchange: options.ExchangeName, + exchange: config.ExchangeName, type: "x-delayed-message", durable: true, autoDelete: false, arguments: args); await channel.QueueDeclareAsync( - queue: options.QueueName, + queue: config.QueueName, durable: true, exclusive: false, autoDelete: false); await channel.QueueBindAsync( - queue: options.QueueName, - exchange: options.ExchangeName, - routingKey: options.RoutingKey); + queue: config.QueueName, + exchange: config.ExchangeName, + routingKey: config.RoutingKey); return channel; } @@ -147,15 +147,15 @@ await channel.QueueBindAsync( private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDeliverEventArgs eventArgs) { var data = string.Empty; - var options = registration.Consumer.Options; + var config = registration.Consumer.Config as RabbitMQConsumerConfig ?? new(); try { data = Encoding.UTF8.GetString(eventArgs.Body.Span); - _logger.LogInformation($"Message received on '{options.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); + _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); - var isDone = await registration.Consumer.HandleMessageAsync(options.QueueName, data); - if (!options.AutoAck && registration.Channel != null) + var isDone = await registration.Consumer.HandleMessageAsync(config.QueueName, data); + if (!config.AutoAck && registration.Channel != null) { if (isDone) { @@ -169,8 +169,8 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel } catch (Exception ex) { - _logger.LogError(ex, $"Error consuming message on queue '{options.QueueName}': {data}"); - if (!options.AutoAck && registration.Channel != null) + _logger.LogError(ex, $"Error consuming message on queue '{config.QueueName}': {data}"); + if (!config.AutoAck && registration.Channel != null) { await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); } @@ -209,7 +209,7 @@ await policy.Execute(async () => } await channel.ExchangeDeclareAsync( - exchange: options.Exchange, + exchange: options.TopicName, type: "x-delayed-message", durable: true, autoDelete: false, @@ -224,12 +224,12 @@ await channel.ExchangeDeclareAsync( DeliveryMode = DeliveryModes.Persistent, Headers = new Dictionary { - ["x-delay"] = options.MilliSeconds + ["x-delay"] = options.DelayMilliseconds } }; await channel.BasicPublishAsync( - exchange: options.Exchange, + exchange: options.TopicName, routingKey: options.RoutingKey, mandatory: true, basicProperties: properties, diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs index 508d3a4ea..0a8a8c3a5 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Using.cs @@ -34,4 +34,5 @@ global using BotSharp.Plugin.RabbitMQ.Settings; global using BotSharp.Plugin.RabbitMQ.Models; global using BotSharp.Plugin.RabbitMQ.Interfaces; -global using BotSharp.Plugin.RabbitMQ.Consumers; \ No newline at end of file +global using BotSharp.Plugin.RabbitMQ.Consumers; +global using BotSharp.Plugin.RabbitMQ.Connections; \ No newline at end of file From 436990d04c55fb7c13f21c8fbf45cbf2fd4b5711 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 22 Jan 2026 21:58:51 -0600 Subject: [PATCH 20/91] fix mq config --- .../MessageQueues/IMQConsumer.cs | 1 - .../MessageQueues/MQConsumerBase.cs | 1 - .../MessageQueues/Models/MQConsumerConfig.cs | 34 ------------------- .../Consumers/DummyMessageConsumer.cs | 2 +- .../Consumers/ScheduledMessageConsumer.cs | 2 +- .../Models/RabbitMQConsumerConfig.cs | 10 +++--- .../RabbitMQPlugin.cs | 2 +- .../Services/RabbitMQService.cs | 11 ++++-- src/WebStarter/appsettings.json | 2 +- 9 files changed, 17 insertions(+), 48 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs index 25fcda5a2..2f9296689 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -21,4 +21,3 @@ public interface IMQConsumer : IDisposable /// True if the message was handled successfully, false otherwise Task HandleMessageAsync(string channel, string data); } - diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index 73b7572bc..2fda58581 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Infrastructures.MessageQueues; -using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; using Microsoft.Extensions.Logging; namespace BotSharp.Plugin.RabbitMQ.Consumers; diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs deleted file mode 100644 index bb3072a14..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQConsumerConfig.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; - -/// -/// Configuration options for message queue consumers. -/// These options are MQ-product agnostic and can be adapted by different implementations. -/// -public class MQConsumerConfig -{ - /// - /// The exchange name (topic in some MQ systems). - /// - public string ExchangeName { get; set; } = string.Empty; - - /// - /// The queue name (subscription in some MQ systems). - /// - public string QueueName { get; set; } = string.Empty; - - /// - /// The routing key (filter in some MQ systems). - /// - public string RoutingKey { get; set; } = string.Empty; - - /// - /// Whether to automatically acknowledge messages. - /// - public bool AutoAck { get; set; } = false; - - /// - /// Additional arguments for the consumer configuration. - /// - public Dictionary Arguments { get; set; } = new(); -} - diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs index bdefa3131..36af0df90 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/DummyMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class DummyMessageConsumer : MQConsumerBase { - public override MQConsumerConfig Config => new() + public override object Config => new RabbitMQConsumerConfig { ExchangeName = "my.exchange", QueueName = "dummy.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs index b28089a94..f6040dcd7 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Consumers/ScheduledMessageConsumer.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.RabbitMQ.Consumers; public class ScheduledMessageConsumer : MQConsumerBase { - public override object Config => new + public override object Config => new RabbitMQConsumerConfig { ExchangeName = "my.exchange", QueueName = "scheduled.queue", diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs index 1790fe1ab..4dc8f8ed5 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs @@ -5,25 +5,25 @@ internal class RabbitMQConsumerConfig /// /// The exchange name (topic in some MQ systems). /// - public string ExchangeName { get; set; } = "rabbitmq.exchange"; + internal string ExchangeName { get; set; } = "rabbitmq.exchange"; /// /// The queue name (subscription in some MQ systems). /// - public string QueueName { get; set; } = "rabbitmq.queue"; + internal string QueueName { get; set; } = "rabbitmq.queue"; /// /// The routing key (filter in some MQ systems). /// - public string RoutingKey { get; set; } = "rabbitmq.routing"; + internal string RoutingKey { get; set; } = "rabbitmq.routing"; /// /// Whether to automatically acknowledge messages. /// - public bool AutoAck { get; set; } = false; + internal bool AutoAck { get; set; } = false; /// /// Additional arguments for the consumer configuration. /// - public Dictionary Arguments { get; set; } = new(); + internal Dictionary Arguments { get; set; } = new(); } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs index 9da987eec..ff45dfe48 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/RabbitMQPlugin.cs @@ -14,7 +14,7 @@ public class RabbitMQPlugin : IBotSharpAppPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { var settings = new RabbitMQSettings(); - config.Bind("RabbitMessageQueue", settings); + config.Bind("RabbitMQ", settings); services.AddSingleton(settings); var mqSettings = new MessageQueueSettings(); diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 96c53f1ad..0ea2465a7 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -154,10 +154,10 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel data = Encoding.UTF8.GetString(eventArgs.Body.Span); _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); - var isDone = await registration.Consumer.HandleMessageAsync(config.QueueName, data); + var isHandled = await registration.Consumer.HandleMessageAsync(config.QueueName, data); if (!config.AutoAck && registration.Channel != null) { - if (isDone) + if (isHandled) { await registration.Channel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false); } @@ -181,6 +181,11 @@ public async Task PublishAsync(T payload, MQPublishOptions options) { try { + if (options == null) + { + return false; + } + if (!_mqConnection.IsConnected) { await _mqConnection.ConnectAsync(); @@ -200,7 +205,7 @@ await policy.Execute(async () => ["x-delayed-type"] = "direct" }; - if (options.Arguments != null) + if (!options.Arguments.IsNullOrEmpty()) { foreach (var kvp in options.Arguments) { diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 62e55d45f..322315192 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1025,7 +1025,7 @@ "Provider": "RabbitMQ" }, - "RabbitMessageQueue": { + "RabbitMQ": { "HostName": "localhost", "Port": 5672, "UserName": "guest", From 528147936830dee0719fd59f79a8978ff8fff128 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Sat, 24 Jan 2026 18:31:11 -0600 Subject: [PATCH 21/91] minor change --- .../Repository/MongoRepository.Conversation.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index 300700a64..83b2f1658 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -634,8 +634,7 @@ public async Task> TruncateConversation(string conversationId, stri continue; } - var values = state.Values.Where(x => x.MessageId != messageId) - .Where(x => x.UpdateTime < refTime) + var values = state.Values.Where(x => x.MessageId != messageId && x.UpdateTime < refTime) .ToList(); if (values.Count == 0) continue; From 77c2adb1092385d2af5264132ecabc68822afb28 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 14:27:19 -0600 Subject: [PATCH 22/91] refien rule action --- .../Agents/Models/AgentRule.cs | 14 ++ .../Rules/Hooks/IRuleTriggerHook.cs | 8 +- .../Rules/Hooks/RuleTriggerHookBase.cs | 13 ++ .../Rules/Models/RuleActionContext.cs | 4 +- .../Rules/Models/RuleHttpContext.cs | 13 -- .../Rules/Models/RuleHttpResult.cs | 6 - .../Rules/Options/RuleTriggerOptions.cs | 1 - .../Utilities/ObjectExtensions.cs | 38 ++++ .../Controllers/RuleController.cs | 42 ++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 81 +++++++- .../Models/RuleMessagePayload.cs | 11 + .../Models/RuleTriggerActionRequest.cs | 18 ++ .../BotSharp.Core.Rules/RulesPlugin.cs | 3 + .../Services/ChatRuleAction.cs | 10 +- .../Services/FunctionCallRuleAction.cs | 47 +++++ .../Services/HttpRuleAction.cs | 191 ++++++++++++++++++ .../Services/MessageQueueRuleAction.cs | 125 ++++++++++++ .../BotSharp.Core.Rules/Using.cs | 2 + .../Controllers/Agent/AgentController.Rule.cs | 7 +- .../Models/AgentRuleMongoElement.cs | 11 +- 20 files changed, 605 insertions(+), 40 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index dfe03681d..6e31e1b71 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Agents.Models; public class AgentRule @@ -13,4 +15,16 @@ public class AgentRule [JsonPropertyName("action")] public string? Action { get; set; } + + /// + /// Adaptive configuration for rule actions. + /// This flexible JSON document can store any action-specific configuration. + /// The structure depends on the action type: + /// - For "Http" action: contains http_context with base_url, relative_url, method, etc. + /// - For "MessageQueue" action: contains mq_config with topic_name, routing_key, etc. + /// - For custom actions: can contain any custom configuration structure + /// + [JsonPropertyName("action_config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonDocument? ActionConfig { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index bfeda4086..08195c89c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -1,14 +1,10 @@ using BotSharp.Abstraction.Hooks; -using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Rules.Models; namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { - Task BeforeSendEventMessage(Agent agent, RoleDialogModel message) => Task.CompletedTask; - Task AfterSendEventMessage(Agent agent, InstructResult result) => Task.CompletedTask; - - Task BeforeSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpContext message) => Task.CompletedTask; - Task AfterSendHttpRequest(Agent agent, IRuleTrigger trigger, RuleHttpResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs index 60bdf7cf5..f3b99cbe8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs @@ -1,5 +1,18 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules.Hooks; public class RuleTriggerHookBase : IRuleTriggerHook { + public string SelfId = string.Empty; + + public Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) + { + return Task.CompletedTask; + } + + public Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) + { + return Task.CompletedTask; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index d6a0d5570..12a0a3e66 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -2,6 +2,6 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { - public string Text { get; set; } - public IEnumerable? States { get; set; } + public string Text { get; set; } = string.Empty; + public Dictionary States { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs deleted file mode 100644 index 0268d12a1..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net.Http; - -namespace BotSharp.Abstraction.Rules.Models; - -public class RuleHttpContext -{ - public string BaseUrl { get; set; } - public string RelativeUrl { get; set; } - public HttpMethod Method { get; set; } - public Dictionary Headers { get; set; } = []; - public Dictionary QueryParams { get; set; } = []; - public string RequestBody { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs deleted file mode 100644 index c5109a5c7..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleHttpResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Models; - -public class RuleHttpResult -{ - public string HttpResponse { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 93703d98f..70567ea6d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -7,4 +7,3 @@ public class RuleTriggerOptions /// public RuleCriteriaOptions? Criteria { get; set; } } - diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs index a36516c8a..9efdf6f42 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs @@ -65,4 +65,42 @@ public static class ObjectExtensions return null; } } + + public static T? TryGetValueOrDefault(this IDictionary dict, string key, T? defaultValue = default, JsonSerializerOptions? jsonOptions = null) + { + return dict.TryGetValue(key, out var value, jsonOptions) + ? value! + : defaultValue; + } + + public static bool TryGetValue(this IDictionary dict, string key, out T? result, JsonSerializerOptions? jsonOptions = null) + { + result = default; + + if (!dict.TryGetValue(key, out var value) || value is null) + { + return false; + } + + if (value is T t) + { + result = t; + return true; + } + + if (value is JsonElement je) + { + try + { + result = je.Deserialize(jsonOptions); + return true; + } + catch + { + return false; + } + } + + return false; + } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs new file mode 100644 index 000000000..ebb2bf0c5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs @@ -0,0 +1,42 @@ +using BotSharp.Core.Rules.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BotSharp.Core.Rules.Controllers; + +[Authorize] +[ApiController] +public class RuleController : ControllerBase +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IRuleEngine _ruleEngine; + + public RuleController( + IServiceProvider services, + ILogger logger, + IRuleEngine ruleEngine) + { + _services = services; + _logger = logger; + _ruleEngine = ruleEngine; + } + + [HttpPost("/rule/trigger/run")] + public async Task RunAction([FromBody] RuleTriggerActionRequest request) + { + if (request == null) + { + return BadRequest(new { Success = false, Error = "Request cannnot be empty." }); + } + + var trigger = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(request.TriggerName)); + if (trigger == null) + { + return BadRequest(new { Success = false, Error = "Unable to find rule trigger." }); + } + + var result = await _ruleEngine.Triggered(trigger, request.Text, request.States, request.Options); + return Ok(new { Success = true }); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6fa72b145..a485e124e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,3 +1,6 @@ +using BotSharp.Abstraction.Rules.Hooks; +using System.Text.Json; + namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -60,9 +63,9 @@ public async Task> Triggered(IRuleTrigger trigger, string te var context = new RuleActionContext { Text = text, - States = states + States = BuildRuleActionContext(foundRule, states) }; - var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("Chat")!, context); + var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("BotSharp-chat")!, context); if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { newConversationIds.Add(result.ConversationId); @@ -72,6 +75,26 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } + private Dictionary BuildRuleActionContext(AgentRule rule, IEnumerable? states) + { + var dict = new Dictionary(); + + if (rule.ActionConfig != null) + { + dict = ConvertToDictionary(rule.ActionConfig); + } + + if (!states.IsNullOrEmpty()) + { + foreach (var state in states!) + { + dict[state.Key] = state.Value; + } + } + + return dict; + } + private async Task ExecuteActionAsync( Agent agent, IRuleTrigger trigger, @@ -96,7 +119,24 @@ private async Task ExecuteActionAsync( _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", action.Name, agent.Id, trigger.Name); - return await action.ExecuteAsync(agent, trigger, context); + // Combine states + + + var hooks = _services.GetHooks(agent.Id); + foreach (var hook in hooks) + { + await hook.BeforeRuleActionExecuted(agent, trigger, context); + } + + // Execute action + var result = await action.ExecuteAsync(agent, trigger, context); + + foreach (var hook in hooks) + { + await hook.AfterRuleActionExecuted(agent, trigger, result); + } + + return result; } catch (Exception ex) { @@ -104,4 +144,39 @@ private async Task ExecuteActionAsync( return RuleActionResult.Failed(ex.Message); } } + + public static Dictionary ConvertToDictionary(JsonDocument doc) + { + var dict = new Dictionary(); + + foreach (var prop in doc.RootElement.EnumerateObject()) + { + dict[prop.Name] = prop.Value.ValueKind switch + { + JsonValueKind.String => prop.Value.GetString(), + JsonValueKind.Number when prop.Value.TryGetInt32(out int intValue) => intValue, + JsonValueKind.Number when prop.Value.TryGetInt64(out long longValue) => longValue, + JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, + JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, + JsonValueKind.Number when prop.Value.TryGetByte(out byte byteValue) => byteValue, + JsonValueKind.Number when prop.Value.TryGetSByte(out sbyte sbyteValue) => sbyteValue, + JsonValueKind.Number when prop.Value.TryGetUInt16(out ushort uint16Value) => uint16Value, + JsonValueKind.Number when prop.Value.TryGetUInt32(out uint uint32Value) => uint32Value, + JsonValueKind.Number when prop.Value.TryGetUInt64(out ulong uint64Value) => uint64Value, + JsonValueKind.Number when prop.Value.TryGetDateTime(out DateTime dateTimeValue) => dateTimeValue, + JsonValueKind.Number when prop.Value.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffsetValue) => dateTimeOffsetValue, + JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue, + JsonValueKind.Number => prop.Value.GetRawText(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Undefined => null, + JsonValueKind.Array => prop.Value, + JsonValueKind.Object => prop.Value, + _ => prop.Value + }; + } + + return dict; + } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs new file mode 100644 index 000000000..1c5e81d71 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleMessagePayload.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Core.Rules.Models; + +public class RuleMessagePayload +{ + public string AgentId { get; set; } + public string TriggerName { get; set; } + public string Channel { get; set; } + public string Text { get; set; } + public Dictionary States { get; set; } + public DateTime Timestamp { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs new file mode 100644 index 000000000..0abea08b0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Models/RuleTriggerActionRequest.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Core.Rules.Models; + +public class RuleTriggerActionRequest +{ + [JsonPropertyName("trigger_name")] + public string TriggerName { get; set; } + + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; + + [JsonPropertyName("states")] + public IEnumerable? States { get; set; } + + [JsonPropertyName("options")] + public RuleTriggerOptions? Options { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index aee5a7fc7..e3403ed30 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -22,5 +22,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) // Register rule actions services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs index 903bcdb00..27ccbf1bf 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs @@ -13,17 +13,20 @@ public ChatRuleAction( _logger = logger; } - public string Name => "Chat"; + public string Name => "BotSharp-chat"; public async Task ExecuteAsync( Agent agent, IRuleTrigger trigger, RuleActionContext context) { + using var scope = _services.CreateScope(); + var sp = scope.ServiceProvider; + try { var channel = trigger.Channel; - var convService = _services.GetRequiredService(); + var convService = sp.GetRequiredService(); var conv = await convService.NewConversation(new Conversation { Channel = channel, @@ -40,7 +43,8 @@ public async Task ExecuteAsync( if (!context.States.IsNullOrEmpty()) { - allStates.AddRange(context.States!); + var states = context.States.Select(x => new MessageState(x.Key, x.Value)); + allStates.AddRange(states); } await convService.SetConversationId(conv.Id, allStates); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs new file mode 100644 index 000000000..4b7068fab --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs @@ -0,0 +1,47 @@ + +using BotSharp.Abstraction.Functions; + +namespace BotSharp.Core.Rules.Services; + +public class FunctionCallRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public FunctionCallRuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Name => "BotSharp-function-call"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + context.States ??= []; + + var funcName = context.States.TryGetValueOrDefault("function_name", string.Empty); + var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); + + if (func == null) + { + var errorMsg = $"Unable to find function '{funcName}' when running action {agent.Name}-{trigger.Name}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + var funcArg = context.States.TryGetValueOrDefault("function_argument") ?? new(); + await func.Execute(funcArg); + + return new RuleActionResult + { + Success = true, + Response = $"Function {funcName} is executed successfully." + }; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs new file mode 100644 index 000000000..6ce6898a8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -0,0 +1,191 @@ +using BotSharp.Abstraction.Options; +using System.Net.Mime; +using System.Text.Json; +using System.Web; + +namespace BotSharp.Core.Rules.Services; + +public sealed class HttpRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + + public HttpRuleAction( + IServiceProvider services, + ILogger logger, + IHttpClientFactory httpClientFactory) + { + _services = services; + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + public string Name => "BotSharp-http"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + context.States ??= []; + + var httpMethod = GetHttpMethod(context); + if (httpMethod == null) + { + var errorMsg = $"HTTP method is not supported in agent rule {agent.Name}-{trigger.Name}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + // Build the full URL + var fullUrl = BuildUrl(context); + + using var client = _httpClientFactory.CreateClient(); + + // Add headers + AddHttpHeaders(client, context); + + // Create request + var request = new HttpRequestMessage(httpMethod, fullUrl); + + // Add request body if provided + var requestBodyStr = GetHttpRequestBody(context); + if (!string.IsNullOrEmpty(requestBodyStr)) + { + request.Content = new StringContent(requestBodyStr, Encoding.UTF8, MediaTypeNames.Application.Json); + } + + _logger.LogInformation("Executing HTTP rule action for agent {AgentId}, URL: {Url}, Method: {Method}", + agent.Id, fullUrl, httpMethod); + + // Send request + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + _logger.LogInformation("HTTP rule action executed successfully for agent {AgentId}, Status: {StatusCode}", + agent.Id, response.StatusCode); + + return new RuleActionResult + { + Success = true, + Response = responseContent + }; + } + else + { + var errorMsg = $"HTTP request failed with status code {response.StatusCode}: {responseContent}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing HTTP rule action for agent {AgentId} and trigger {TriggerName}", + agent.Id, trigger.Name); + return RuleActionResult.Failed(ex.Message); + } + } + + private string BuildUrl(RuleActionContext context) + { + var url = context.States.TryGetValueOrDefault("http_url"); + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentNullException("Unable to find http_url in context"); + } + + // Fill in placeholders in url + var strValues = context.States.Where(x => x.Value != null && x.Value is string); + foreach (var item in strValues) + { + var value = item.Value as string; + if (string.IsNullOrEmpty(value)) + { + continue; + } + url.Replace($"{{{item.Key}}}", value); + } + + + var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); + + // Add query parameters + if (!queryParams.IsNullOrEmpty()) + { + var builder = new UriBuilder(url); + var query = HttpUtility.ParseQueryString(builder.Query); + + // Add new query params + foreach (var kv in queryParams!.Where(x => x.Value != null)) + { + query[kv.Key] = kv.Value!; + } + + // Assign merged query back + builder.Query = query.ToString(); + url = builder.ToString(); + } + + return url; + } + + private HttpMethod? GetHttpMethod(RuleActionContext context) + { + var method = context.States.TryGetValueOrDefault("http_method", string.Empty); + var innerMethod = method?.Trim()?.ToUpper(); + HttpMethod? matchMethod = null; + + switch (innerMethod) + { + case "GET": + matchMethod = HttpMethod.Get; + break; + case "POST": + matchMethod = HttpMethod.Post; + break; + case "DELETE": + matchMethod = HttpMethod.Delete; + break; + case "PUT": + matchMethod = HttpMethod.Put; + break; + case "PATCH": + matchMethod = HttpMethod.Patch; + break; + default: + break; + + } + + return matchMethod; + } + + private void AddHttpHeaders(HttpClient client, RuleActionContext context) + { + var headerParams = context.States.TryGetValueOrDefault>("http_headers"); + if (!headerParams.IsNullOrEmpty()) + { + foreach (var header in headerParams!) + { + client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value); + } + } + } + + private string? GetHttpRequestBody(RuleActionContext context) + { + var body = context.States.TryGetValueOrDefault("http_request_body"); + if (string.IsNullOrEmpty(body)) + { + return null; + } + + return JsonSerializer.Serialize(body, BotSharpOptions.defaultJsonOptions); + } +} + diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs new file mode 100644 index 000000000..4c0c72893 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs @@ -0,0 +1,125 @@ +using BotSharp.Core.Rules.Models; + +namespace BotSharp.Core.Rules.Services; + +public sealed class MessageQueueRuleAction : IRuleAction +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public MessageQueueRuleAction( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Name => "BotSharp-message-queue"; + + public async Task ExecuteAsync( + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + context.States ??= []; + + // Get message queue service + var mqService = _services.GetService(); + if (mqService == null) + { + var errorMsg = "Message queue service is not configured. Please ensure a message queue provider (e.g., RabbitMQ) is registered."; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + // Create message payload + var payload = new RuleMessagePayload + { + AgentId = agent.Id, + TriggerName = trigger.Name, + Channel = trigger.Channel, + Text = context.Text, + Timestamp = DateTime.UtcNow, + States = context.States + }; + + // Publish message to queue + var mqOptions = GetMQPublishOptions(context); + var success = await mqService.PublishAsync(payload, mqOptions); + + if (success) + { + _logger.LogInformation("MessageQueue rule action executed successfully for agent {AgentId}", agent.Id); + return new RuleActionResult + { + Success = true, + Response = $"Message published to queue: {mqOptions.TopicName}-{mqOptions.RoutingKey}" + }; + } + else + { + var errorMsg = $"Failed to publish message to queue {mqOptions.TopicName}-{mqOptions.RoutingKey}"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing MessageQueue rule action for agent {AgentId} and trigger {TriggerName}", + agent.Id, trigger.Name); + return RuleActionResult.Failed(ex.Message); + } + } + + private MQPublishOptions GetMQPublishOptions(RuleActionContext context) + { + var topicName = context.States.TryGetValueOrDefault("mq_topic_name", string.Empty); + var routingKey = context.States.TryGetValueOrDefault("mq_routing_key", string.Empty); + var delayMilliseconds = ParseDelay(context); + + return new MQPublishOptions + { + TopicName = topicName, + RoutingKey = routingKey, + DelayMilliseconds = delayMilliseconds + }; + } + + private long ParseDelay(RuleActionContext context) + { + var qty = (double)context.States.TryGetValueOrDefault("mq_delay_qty", 0); + if (qty == 0) + { + qty = context.States.TryGetValueOrDefault("mq_delay_qty", 0.0); + } + + var unit = context.States.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; + unit = unit.ToLower(); + + var milliseconds = 0L; + switch (unit) + { + case "second": + case "seconds": + milliseconds = (long)TimeSpan.FromSeconds(qty).TotalMilliseconds; + break; + case "minute": + case "minutes": + milliseconds = (long)TimeSpan.FromMilliseconds(qty).TotalMilliseconds; + break; + case "hour": + case "hours": + milliseconds = (long)TimeSpan.FromHours(qty).TotalMilliseconds; + break; + case "day": + case "days": + milliseconds = (long)TimeSpan.FromDays(qty).TotalMilliseconds; + break; + } + + return milliseconds; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index 2cfb617d2..19982448e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -1,6 +1,7 @@ global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; +global using System.Text; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Plugins; @@ -13,6 +14,7 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Infrastructures.MessageQueues; +global using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Repositories.Filters; global using BotSharp.Abstraction.Rules; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 52fd719fd..0e4cc9dd0 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Rules; namespace BotSharp.OpenAPI.Controllers; @@ -18,9 +17,9 @@ public IEnumerable GetRuleTriggers() }).OrderBy(x => x.TriggerName); } - [HttpGet("/rule/formalization")] - public async Task GetFormalizedRuleDefinition([FromBody] AgentRule rule) + [HttpGet("/rule/actions")] + public async Task> GetRuleActions() { - return "{}"; + return _services.GetServices().Select(x => x.Name).OrderBy(x => x); } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 4205fdc46..0fd34fa6e 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -8,6 +9,8 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public string Criteria { get; set; } = default!; + public string? Action { get; set; } + public BsonDocument? ActionConfig { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -15,7 +18,9 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria + Criteria = rule.Criteria, + Action = rule.Action, + ActionConfig = rule.ActionConfig != null ? BsonDocument.Parse(rule.ActionConfig.RootElement.GetRawText()) : null }; } @@ -25,7 +30,9 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria + Criteria = rule.Criteria, + Action = rule.Action, + ActionConfig = rule.ActionConfig != null ? JsonDocument.Parse(rule.ActionConfig.ToJson()) : null }; } } From 9341685421f856cc0d8bd3a08b80846a6b27960b Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 14:30:49 -0600 Subject: [PATCH 23/91] replace url --- .../BotSharp.Core.Rules/Services/HttpRuleAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs index 6ce6898a8..c7a2611f3 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -108,7 +108,7 @@ private string BuildUrl(RuleActionContext context) { continue; } - url.Replace($"{{{item.Key}}}", value); + url = url.Replace($"{{{item.Key}}}", value); } From dc5e2e62294d2d7db0a21110e632daaf8910ef9e Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 14:35:43 -0600 Subject: [PATCH 24/91] avoid negative delay qty --- .../BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs index 4c0c72893..b32f25e3c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs @@ -96,6 +96,11 @@ private long ParseDelay(RuleActionContext context) qty = context.States.TryGetValueOrDefault("mq_delay_qty", 0.0); } + if (qty <= 0) + { + return 0L; + } + var unit = context.States.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; unit = unit.ToLower(); From f73598be1a698de9df756fe2dd02ddae88e328ce Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 15:30:36 -0600 Subject: [PATCH 25/91] add agent rule action --- .../Agents/Models/AgentRule.cs | 18 +++++-- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 17 ++++--- .../Models/AgentRuleMongoElement.cs | 47 ++++++++++++++++--- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 6e31e1b71..2e87d918a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -14,7 +14,17 @@ public class AgentRule public string Criteria { get; set; } = string.Empty; [JsonPropertyName("action")] - public string? Action { get; set; } + public AgentRuleAction? Action { get; set; } +} + + +public class AgentRuleAction +{ + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("disabled")] + public bool Disabled { get; set; } /// /// Adaptive configuration for rule actions. @@ -24,7 +34,7 @@ public class AgentRule /// - For "MessageQueue" action: contains mq_config with topic_name, routing_key, etc. /// - For custom actions: can contain any custom configuration structure /// - [JsonPropertyName("action_config")] + [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonDocument? ActionConfig { get; set; } -} + public JsonDocument? Config { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index a485e124e..e49ac968e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -55,7 +55,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundRule == null) + if (foundRule == null || foundRule.Action?.Disabled == false) { continue; } @@ -63,9 +63,11 @@ public async Task> Triggered(IRuleTrigger trigger, string te var context = new RuleActionContext { Text = text, - States = BuildRuleActionContext(foundRule, states) + States = BuildRuleActionContext(foundRule.Action, states) }; - var result = await ExecuteActionAsync(agent, trigger, foundRule.Action.IfNullOrEmptyAs("BotSharp-chat")!, context); + + var action = foundRule?.Action?.Name ?? "BotSharp-chat"; + var result = await ExecuteActionAsync(agent, trigger, action, context); if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { newConversationIds.Add(result.ConversationId); @@ -75,13 +77,13 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } - private Dictionary BuildRuleActionContext(AgentRule rule, IEnumerable? states) + private Dictionary BuildRuleActionContext(AgentRuleAction? ruleAction, IEnumerable? states) { var dict = new Dictionary(); - if (rule.ActionConfig != null) + if (ruleAction?.Config != null) { - dict = ConvertToDictionary(rule.ActionConfig); + dict = ConvertToDictionary(ruleAction.Config); } if (!states.IsNullOrEmpty()) @@ -119,9 +121,6 @@ private async Task ExecuteActionAsync( _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", action.Name, agent.Id, trigger.Name); - // Combine states - - var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 0fd34fa6e..dc1c4671b 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -9,8 +9,7 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public string Criteria { get; set; } = default!; - public string? Action { get; set; } - public BsonDocument? ActionConfig { get; set; } + public AgentRuleActionMongModel? Action { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -19,8 +18,7 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = rule.Action, - ActionConfig = rule.ActionConfig != null ? BsonDocument.Parse(rule.ActionConfig.RootElement.GetRawText()) : null + Action = AgentRuleActionMongModel.ToMongoModel(rule.Action) }; } @@ -31,8 +29,45 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = rule.Action, - ActionConfig = rule.ActionConfig != null ? JsonDocument.Parse(rule.ActionConfig.ToJson()) : null + Action = AgentRuleActionMongModel.ToDomainModel(rule.Action) }; } } + + +public class AgentRuleActionMongModel +{ + public string Name { get; set; } + public bool Disabled { get; set; } + public BsonDocument? Config { get; set; } + + public static AgentRuleActionMongModel? ToMongoModel(AgentRuleAction? action) + { + if (action == null) + { + return null; + } + + return new AgentRuleActionMongModel + { + Name = action.Name, + Disabled = action.Disabled, + Config = action.Config != null ? BsonDocument.Parse(action.Config.RootElement.GetRawText()) : null + }; + } + + public static AgentRuleAction? ToDomainModel(AgentRuleActionMongModel? action) + { + if (action == null) + { + return null; + } + + return new AgentRuleAction + { + Name = action.Name, + Disabled = action.Disabled, + Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null + }; + } +} \ No newline at end of file From 2ea54831684adca384baa25285c52738fbfc69aa Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 16:32:35 -0600 Subject: [PATCH 26/91] ignore action when null --- .../BotSharp.Abstraction/Agents/Models/AgentRule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 2e87d918a..07ae67b77 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -14,6 +14,7 @@ public class AgentRule public string Criteria { get; set; } = string.Empty; [JsonPropertyName("action")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentRuleAction? Action { get; set; } } From 3d1d0eaff3653272412da308af23b4b5baaaf69e Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 26 Jan 2026 16:45:59 -0600 Subject: [PATCH 27/91] return function calling response --- .../BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs index 4b7068fab..be24697cf 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs @@ -41,7 +41,7 @@ public async Task ExecuteAsync( return new RuleActionResult { Success = true, - Response = $"Function {funcName} is executed successfully." + Response = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content }; } } From 4b7f5ac47fe22037202e1118bc5b1c96d7638571 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 27 Jan 2026 12:09:14 -0600 Subject: [PATCH 28/91] refine http rule --- .../Rules/Hooks/RuleTriggerHookBase.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 2 +- .../BotSharp.Core.Rules/Services/HttpRuleAction.cs | 9 +++------ .../Models/AgentRuleMongoElement.cs | 2 +- src/WebStarter/appsettings.json | 1 + 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs index f3b99cbe8..44b85a29f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs @@ -4,7 +4,7 @@ namespace BotSharp.Abstraction.Rules.Hooks; public class RuleTriggerHookBase : IRuleTriggerHook { - public string SelfId = string.Empty; + public string SelfId => string.Empty; public Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index e49ac968e..6cbf00f6b 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -55,7 +55,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundRule == null || foundRule.Action?.Disabled == false) + if (foundRule == null || foundRule.Action?.Disabled == true) { continue; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs index c7a2611f3..5b2e0a6d8 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -100,10 +100,9 @@ private string BuildUrl(RuleActionContext context) } // Fill in placeholders in url - var strValues = context.States.Where(x => x.Value != null && x.Value is string); - foreach (var item in strValues) + foreach (var item in context.States) { - var value = item.Value as string; + var value = item.Value?.ToString(); if (string.IsNullOrEmpty(value)) { continue; @@ -111,10 +110,8 @@ private string BuildUrl(RuleActionContext context) url = url.Replace($"{{{item.Key}}}", value); } - - var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); - // Add query parameters + var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); if (!queryParams.IsNullOrEmpty()) { var builder = new UriBuilder(url); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index dc1c4671b..ce25d121f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -34,7 +34,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) } } - +[BsonIgnoreExtraElements(Inherited = true)] public class AgentRuleActionMongModel { public string Name { get; set; } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 322315192..b3c2b35e9 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1039,6 +1039,7 @@ "BotSharp.Core.A2A", "BotSharp.Core.SideCar", "BotSharp.Core.Crontab", + "BotSharp.Core.Rules", "BotSharp.Core.Realtime", "BotSharp.Logger", "BotSharp.Plugin.MongoStorage", From 0b97b67a5071d2c09f6b300339641de0d35a4cd6 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 27 Jan 2026 19:12:06 -0600 Subject: [PATCH 29/91] add agent filter to rule trigger options --- .../Rules/Options/RuleTriggerOptions.cs | 7 +++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 20 ++++++++++++------- .../Services/FunctionCallRuleAction.cs | 2 -- .../Services/HttpRuleAction.cs | 6 ++---- .../Services/MessageQueueRuleAction.cs | 2 -- .../Services/RuleCriteria.cs | 2 +- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 70567ea6d..e01513f51 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,7 +1,14 @@ +using BotSharp.Abstraction.Repositories.Filters; + namespace BotSharp.Abstraction.Rules.Options; public class RuleTriggerOptions { + /// + /// Filter agents + /// + public AgentFilter? AgentFilter { get; set; } + /// /// Criteria options for validating whether the rule should be triggered /// diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6cbf00f6b..a5e5e7b6a 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -22,7 +22,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Pull all user defined rules var agentService = _services.GetRequiredService(); - var agents = await agentService.GetAgents(new AgentFilter + var agents = await agentService.GetAgents(options?.AgentFilter ?? new AgentFilter { Pager = new Pagination { @@ -34,11 +34,17 @@ public async Task> Triggered(IRuleTrigger trigger, string te var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); foreach (var agent in filteredAgents) { + var rule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); + if (rule == null) + { + continue; + } + // Criteria validation if (options?.Criteria != null) { var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "botsharp-rule-criteria")); + .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "BotSharp-rule-criteria")); if (criteria == null) { @@ -49,13 +55,12 @@ public async Task> Triggered(IRuleTrigger trigger, string te var isValid = await criteria.ValidateAsync(agent, trigger, options.Criteria); if (!isValid) { - _logger.LogDebug("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); + _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; } } - var foundRule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled); - if (foundRule == null || foundRule.Action?.Disabled == true) + if (rule.Action?.Disabled == true) { continue; } @@ -63,10 +68,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te var context = new RuleActionContext { Text = text, - States = BuildRuleActionContext(foundRule.Action, states) + States = BuildRuleActionContext(rule.Action, states) }; - var action = foundRule?.Action?.Name ?? "BotSharp-chat"; + var action = rule?.Action?.Name ?? "BotSharp-chat"; var result = await ExecuteActionAsync(agent, trigger, action, context); if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) { @@ -128,6 +133,7 @@ private async Task ExecuteActionAsync( } // Execute action + context.States ??= []; var result = await action.ExecuteAsync(agent, trigger, context); foreach (var hook in hooks) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs index be24697cf..808b4a182 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs @@ -23,8 +23,6 @@ public async Task ExecuteAsync( IRuleTrigger trigger, RuleActionContext context) { - context.States ??= []; - var funcName = context.States.TryGetValueOrDefault("function_name", string.Empty); var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs index 5b2e0a6d8..042cd6d4b 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs @@ -30,8 +30,6 @@ public async Task ExecuteAsync( { try { - context.States ??= []; - var httpMethod = GetHttpMethod(context); if (httpMethod == null) { @@ -176,8 +174,8 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) private string? GetHttpRequestBody(RuleActionContext context) { - var body = context.States.TryGetValueOrDefault("http_request_body"); - if (string.IsNullOrEmpty(body)) + var body = context.States.GetValueOrDefault("http_request_body"); + if (body == null) { return null; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs index b32f25e3c..403b63af8 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs @@ -24,8 +24,6 @@ public async Task ExecuteAsync( { try { - context.States ??= []; - // Get message queue service var mqService = _services.GetService(); if (mqService == null) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs index 015c6e910..c5802385c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs @@ -18,7 +18,7 @@ public RuleCriteria( _codingSettings = codingSettings; } - public string Provider => "botsharp-rule-criteria"; + public string Provider => "BotSharp-rule-criteria"; public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) { From cd68f58409459382d76d435caf71d44fd96d51c4 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 28 Jan 2026 12:01:34 -0600 Subject: [PATCH 30/91] refine rule criteria --- .../Agents/Models/AgentRule.cs | 42 ++++-- .../Rules/Hooks/IRuleTriggerHook.cs | 3 + .../Rules/Hooks/RuleTriggerHookBase.cs | 18 --- .../Rules/IRuleCriteria.cs | 6 +- .../Rules/Models/RuleActionContext.cs | 2 +- .../Rules/Models/RuleCriteriaContext.cs | 7 + .../Rules/Models/RuleCriteriaResult.cs | 19 +++ .../Rules/Options/RuleCriteriaOptions.cs | 5 + .../Rules/Options/RuleTriggerOptions.cs | 5 - .../Constants/RuleConstant.cs | 7 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 123 ++++++++++++------ .../BotSharp.Core.Rules/RulesPlugin.cs | 5 +- .../Services/{ => Actions}/ChatRuleAction.cs | 8 +- .../{ => Actions}/FunctionCallRuleAction.cs | 7 +- .../Services/{ => Actions}/HttpRuleAction.cs | 18 +-- .../{ => Actions}/MessageQueueRuleAction.cs | 14 +- .../CodeScriptRuleCriteria.cs} | 49 +++---- .../BotSharp.Core.Rules/Using.cs | 3 + .../Models/AgentRuleMongoElement.cs | 62 +++++++-- 19 files changed, 271 insertions(+), 132 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/ChatRuleAction.cs (88%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/FunctionCallRuleAction.cs (82%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/HttpRuleAction.cs (88%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{ => Actions}/MessageQueueRuleAction.cs (87%) rename src/Infrastructure/BotSharp.Core.Rules/Services/{RuleCriteria.cs => Criteria/CodeScriptRuleCriteria.cs} (68%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 07ae67b77..635b1628a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -13,20 +13,29 @@ public class AgentRule [JsonPropertyName("criteria")] public string Criteria { get; set; } = string.Empty; - [JsonPropertyName("action")] + [JsonPropertyName("rule_criteria")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public AgentRuleAction? Action { get; set; } -} + public AgentRuleCriteria? RuleCriteria { get; set; } + [JsonPropertyName("rule_action")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AgentRuleAction? RuleAction { get; set; } +} -public class AgentRuleAction +public class AgentRuleCriteria : AgentRuleConfigBase { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("disabled")] - public bool Disabled { get; set; } + /// + /// Adaptive configuration for rule criteria. + /// This flexible JSON document can store any criteria-specific configuration. + /// The structure depends on the criteria executor + /// + [JsonPropertyName("config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public override JsonDocument? Config { get; set; } +} +public class AgentRuleAction : AgentRuleConfigBase +{ /// /// Adaptive configuration for rule actions. /// This flexible JSON document can store any action-specific configuration. @@ -37,5 +46,18 @@ public class AgentRuleAction /// [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public JsonDocument? Config { get; set; } + public override JsonDocument? Config { get; set; } +} + +public class AgentRuleConfigBase +{ + [JsonPropertyName("name")] + public virtual string Name { get; set; } + + [JsonPropertyName("disabled")] + public virtual bool Disabled { get; set; } + + [JsonPropertyName("config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public virtual JsonDocument? Config { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 08195c89c..9e9817890 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -5,6 +5,9 @@ namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { + Task BeforeRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; + Task AfterRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs deleted file mode 100644 index 44b85a29f..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/RuleTriggerHookBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -using BotSharp.Abstraction.Rules.Models; - -namespace BotSharp.Abstraction.Rules.Hooks; - -public class RuleTriggerHookBase : IRuleTriggerHook -{ - public string SelfId => string.Empty; - - public Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) - { - return Task.CompletedTask; - } - - public Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) - { - return Task.CompletedTask; - } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs index af5d5cf3d..f94e0b36f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs @@ -1,9 +1,11 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules; public interface IRuleCriteria { string Provider { get; } - Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) - => Task.FromResult(false); + Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) + => Task.FromResult(new RuleCriteriaResult()); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 12a0a3e66..ca9c6045c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -3,5 +3,5 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { public string Text { get; set; } = string.Empty; - public Dictionary States { get; set; } = []; + public Dictionary Parameters { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs new file mode 100644 index 000000000..d777db1d2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleCriteriaContext +{ + public string Text { get; set; } = string.Empty; + public Dictionary Parameters { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs new file mode 100644 index 000000000..fc1df6ceb --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleCriteriaResult +{ + /// + /// Whether the criteria executed successfully + /// + public bool Success { get; set; } + + /// + /// Response content from the action + /// + public bool IsValid { get; set; } + + /// + /// Error message if the criteria failed + /// + public string? ErrorMessage { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs index 30c820f97..27b5167f4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs @@ -31,4 +31,9 @@ public class CriteriaExecuteOptions /// Json arguments as an input value to the code script /// public JsonDocument? ArgumentContent { get; set; } + + /// + /// Custom parameters + /// + public Dictionary Parameters { get; set; } = []; } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index e01513f51..a2ab78c85 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -8,9 +8,4 @@ public class RuleTriggerOptions /// Filter agents /// public AgentFilter? AgentFilter { get; set; } - - /// - /// Criteria options for validating whether the rule should be triggered - /// - public RuleCriteriaOptions? Criteria { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs new file mode 100644 index 000000000..e9c180120 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Core.Rules.Constants; + +public static class RuleConstant +{ + public const string DEFAULT_CRITERIA_PROVIDER = "BotSharp-code-script"; + public const string DEFAULT_ACTION_NAME = "BotSharp-chat"; +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index a5e5e7b6a..4241c220d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,4 +1,4 @@ -using BotSharp.Abstraction.Rules.Hooks; +using System.Data; using System.Text.Json; namespace BotSharp.Core.Rules.Engines; @@ -41,67 +41,92 @@ public async Task> Triggered(IRuleTrigger trigger, string te } // Criteria validation - if (options?.Criteria != null) + if (rule.RuleCriteria != null && !rule.RuleCriteria.Disabled) { - var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == (options?.Criteria?.Provider ?? "BotSharp-rule-criteria")); - - if (criteria == null) + var criteriaContext = new RuleCriteriaContext { - _logger.LogWarning("No criteria provider found for {Provider}, skipping agent {AgentId}", options.Criteria.Provider, agent.Id); - continue; - } - - var isValid = await criteria.ValidateAsync(agent, trigger, options.Criteria); - if (!isValid) + Text = text, + Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) + }; + var criteriaResult = await ExecuteCriteriaAsync(agent, trigger, rule.RuleCriteria?.Name, criteriaContext); + if (criteriaResult == null || !criteriaResult.IsValid) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; } } - - if (rule.Action?.Disabled == true) + + // Execute action + if (rule.RuleAction?.Disabled == true) { - continue; + continue; } - var context = new RuleActionContext + var actionContext = new RuleActionContext { Text = text, - States = BuildRuleActionContext(rule.Action, states) + Parameters = BuildContextParameters(rule.RuleAction?.Config, states) }; - var action = rule?.Action?.Name ?? "BotSharp-chat"; - var result = await ExecuteActionAsync(agent, trigger, action, context); - if (result.Success && !string.IsNullOrEmpty(result.ConversationId)) + var action = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; + var actionResult = await ExecuteActionAsync(agent, trigger, action, actionContext); + if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { - newConversationIds.Add(result.ConversationId); + newConversationIds.Add(actionResult.ConversationId); } } return newConversationIds; } - private Dictionary BuildRuleActionContext(AgentRuleAction? ruleAction, IEnumerable? states) - { - var dict = new Dictionary(); - if (ruleAction?.Config != null) - { - dict = ConvertToDictionary(ruleAction.Config); - } - - if (!states.IsNullOrEmpty()) + #region Criteria + private async Task ExecuteCriteriaAsync( + Agent agent, + IRuleTrigger trigger, + string? criteriaProvider, + RuleCriteriaContext context) + { + try { - foreach (var state in states!) + var criteria = _services.GetServices() + .FirstOrDefault(x => x.Provider == criteriaProvider); + + if (criteria == null) { - dict[state.Key] = state.Value; + return null; + } + + _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", + criteria.Provider, agent.Id, trigger.Name); + + var hooks = _services.GetHooks(agent.Id); + foreach (var hook in hooks) + { + await hook.BeforeRuleCriteriaExecuted(agent, trigger, context); } + + // Execute criteria + context.Parameters ??= []; + var result = await criteria.ValidateAsync(agent, trigger, context); + + foreach (var hook in hooks) + { + await hook.AfterRuleCriteriaExecuted(agent, trigger, result); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", criteriaProvider ?? string.Empty, agent.Id); + return null; } - - return dict; } + #endregion + + #region Action private async Task ExecuteActionAsync( Agent agent, IRuleTrigger trigger, @@ -133,8 +158,8 @@ private async Task ExecuteActionAsync( } // Execute action - context.States ??= []; - var result = await action.ExecuteAsync(agent, trigger, context); + context.Parameters ??= []; + var result = await action.ExecuteAsync(agent, trigger, context); foreach (var hook in hooks) { @@ -149,8 +174,31 @@ private async Task ExecuteActionAsync( return RuleActionResult.Failed(ex.Message); } } + #endregion + + + #region Private methods + private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states) + { + var dict = new Dictionary(); + + if (config != null) + { + dict = ConvertToDictionary(config); + } + + if (!states.IsNullOrEmpty()) + { + foreach (var state in states!) + { + dict[state.Key] = state.Value; + } + } + + return dict; + } - public static Dictionary ConvertToDictionary(JsonDocument doc) + private Dictionary ConvertToDictionary(JsonDocument doc) { var dict = new Dictionary(); @@ -183,5 +231,6 @@ JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue } return dict; + #endregion } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index e3403ed30..5b8ade6b0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,5 +1,6 @@ using BotSharp.Core.Rules.Engines; -using BotSharp.Core.Rules.Services; +using BotSharp.Core.Rules.Services.Actions; +using BotSharp.Core.Rules.Services.Criteria; namespace BotSharp.Core.Rules; @@ -18,7 +19,7 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { services.AddScoped(); - services.AddScoped(); + services.AddScoped(); // Register rule actions services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs similarity index 88% rename from src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs index 27ccbf1bf..4c599a3e9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public sealed class ChatRuleAction : IRuleAction { @@ -13,7 +13,7 @@ public ChatRuleAction( _logger = logger; } - public string Name => "BotSharp-chat"; + public string Name => RuleConstant.DEFAULT_ACTION_NAME; public async Task ExecuteAsync( Agent agent, @@ -41,9 +41,9 @@ public async Task ExecuteAsync( new("channel", channel) }; - if (!context.States.IsNullOrEmpty()) + if (!context.Parameters.IsNullOrEmpty()) { - var states = context.States.Select(x => new MessageState(x.Key, x.Value)); + var states = context.Parameters.Select(x => new MessageState(x.Key, x.Value)); allStates.AddRange(states); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs similarity index 82% rename from src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs index 808b4a182..6e01f71f4 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs @@ -1,7 +1,6 @@ - using BotSharp.Abstraction.Functions; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public class FunctionCallRuleAction : IRuleAction { @@ -23,7 +22,7 @@ public async Task ExecuteAsync( IRuleTrigger trigger, RuleActionContext context) { - var funcName = context.States.TryGetValueOrDefault("function_name", string.Empty); + var funcName = context.Parameters.TryGetValueOrDefault("function_name", string.Empty); var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); if (func == null) @@ -33,7 +32,7 @@ public async Task ExecuteAsync( return RuleActionResult.Failed(errorMsg); } - var funcArg = context.States.TryGetValueOrDefault("function_argument") ?? new(); + var funcArg = context.Parameters.TryGetValueOrDefault("function_argument") ?? new(); await func.Execute(funcArg); return new RuleActionResult diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs similarity index 88% rename from src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs index 042cd6d4b..f1d2163ce 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Web; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public sealed class HttpRuleAction : IRuleAction { @@ -91,25 +91,25 @@ public async Task ExecuteAsync( private string BuildUrl(RuleActionContext context) { - var url = context.States.TryGetValueOrDefault("http_url"); + var url = context.Parameters.TryGetValueOrDefault("http_url"); if (string.IsNullOrEmpty(url)) { throw new ArgumentNullException("Unable to find http_url in context"); } // Fill in placeholders in url - foreach (var item in context.States) + foreach (var param in context.Parameters) { - var value = item.Value?.ToString(); + var value = param.Value?.ToString(); if (string.IsNullOrEmpty(value)) { continue; } - url = url.Replace($"{{{item.Key}}}", value); + url = url.Replace($"{{{param.Key}}}", value); } // Add query parameters - var queryParams = context.States.TryGetValueOrDefault>("http_query_params"); + var queryParams = context.Parameters.TryGetValueOrDefault>("http_query_params"); if (!queryParams.IsNullOrEmpty()) { var builder = new UriBuilder(url); @@ -131,7 +131,7 @@ private string BuildUrl(RuleActionContext context) private HttpMethod? GetHttpMethod(RuleActionContext context) { - var method = context.States.TryGetValueOrDefault("http_method", string.Empty); + var method = context.Parameters.TryGetValueOrDefault("http_method", string.Empty); var innerMethod = method?.Trim()?.ToUpper(); HttpMethod? matchMethod = null; @@ -162,7 +162,7 @@ private string BuildUrl(RuleActionContext context) private void AddHttpHeaders(HttpClient client, RuleActionContext context) { - var headerParams = context.States.TryGetValueOrDefault>("http_headers"); + var headerParams = context.Parameters.TryGetValueOrDefault>("http_headers"); if (!headerParams.IsNullOrEmpty()) { foreach (var header in headerParams!) @@ -174,7 +174,7 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) private string? GetHttpRequestBody(RuleActionContext context) { - var body = context.States.GetValueOrDefault("http_request_body"); + var body = context.Parameters.GetValueOrDefault("http_request_body"); if (body == null) { return null; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs similarity index 87% rename from src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs index 403b63af8..fcd406b55 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs @@ -1,6 +1,6 @@ using BotSharp.Core.Rules.Models; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Actions; public sealed class MessageQueueRuleAction : IRuleAction { @@ -41,7 +41,7 @@ public async Task ExecuteAsync( Channel = trigger.Channel, Text = context.Text, Timestamp = DateTime.UtcNow, - States = context.States + States = context.Parameters }; // Publish message to queue @@ -74,8 +74,8 @@ public async Task ExecuteAsync( private MQPublishOptions GetMQPublishOptions(RuleActionContext context) { - var topicName = context.States.TryGetValueOrDefault("mq_topic_name", string.Empty); - var routingKey = context.States.TryGetValueOrDefault("mq_routing_key", string.Empty); + var topicName = context.Parameters.TryGetValueOrDefault("mq_topic_name", string.Empty); + var routingKey = context.Parameters.TryGetValueOrDefault("mq_routing_key", string.Empty); var delayMilliseconds = ParseDelay(context); return new MQPublishOptions @@ -88,10 +88,10 @@ private MQPublishOptions GetMQPublishOptions(RuleActionContext context) private long ParseDelay(RuleActionContext context) { - var qty = (double)context.States.TryGetValueOrDefault("mq_delay_qty", 0); + var qty = (double)context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0); if (qty == 0) { - qty = context.States.TryGetValueOrDefault("mq_delay_qty", 0.0); + qty = context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0.0); } if (qty <= 0) @@ -99,7 +99,7 @@ private long ParseDelay(RuleActionContext context) return 0L; } - var unit = context.States.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; + var unit = context.Parameters.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; unit = unit.ToLower(); var milliseconds = 0L; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs similarity index 68% rename from src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs rename to src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs index c5802385c..ad3489bc6 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/RuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs @@ -1,16 +1,16 @@ using System.Text.Json; -namespace BotSharp.Core.Rules.Services; +namespace BotSharp.Core.Rules.Services.Criteria; -public class RuleCriteria : IRuleCriteria +public class CodeScriptRuleCriteria : IRuleCriteria { private readonly IServiceProvider _services; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly CodingSettings _codingSettings; - public RuleCriteria( + public CodeScriptRuleCriteria( IServiceProvider services, - ILogger logger, + ILogger logger, CodingSettings codingSettings) { _services = services; @@ -18,41 +18,45 @@ public RuleCriteria( _codingSettings = codingSettings; } - public string Provider => "BotSharp-rule-criteria"; + public string Provider => RuleConstant.DEFAULT_CRITERIA_PROVIDER; - public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, CriteriaExecuteOptions options) + public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) { + var result = new RuleCriteriaResult(); + if (string.IsNullOrWhiteSpace(agent?.Id)) { - return false; + return result; } - var provider = options.CodeProcessor ?? BuiltInCodeProcessor.PyInterpreter; + var provider = context.Parameters.TryGetValueOrDefault("code_processor") ?? BuiltInCodeProcessor.PyInterpreter; var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); if (processor == null) { _logger.LogWarning($"Unable to find code processor: {provider}."); - return false; + return result; } var agentService = _services.GetRequiredService(); - var scriptName = options.CodeScriptName ?? $"{trigger.Name}_rule.py"; + var scriptName = context.Parameters.TryGetValueOrDefault("code_script_name") ?? $"{trigger.Name}_rule.py"; var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name}) => args: {options.ArgumentContent?.RootElement.GetRawText()}."; + var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name})."; if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) { _logger.LogWarning($"Unable to find {msg}."); - return false; + return result; } try { var hooks = _services.GetHooks(agent.Id); - var arguments = BuildArguments(options.ArgumentName, options.ArgumentContent); - var context = new CodeExecutionContext + var argName = context.Parameters.TryGetValueOrDefault("argument_name"); + var argValue = context.Parameters.TryGetValueOrDefault("argument_value"); + var arguments = BuildArguments(argName, argValue); + var codeExeContext = new CodeExecutionContext { CodeScript = codeScript, Arguments = arguments @@ -60,7 +64,7 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, Criteri foreach (var hook in hooks) { - await hook.BeforeCodeExecution(agent, context); + await hook.BeforeCodeExecution(agent, codeExeContext); } var (useLock, useProcess, timeoutSeconds) = CodingUtil.GetCodeExecutionConfig(_codingSettings); @@ -89,20 +93,19 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, Criteri if (response == null || !response.Success) { _logger.LogWarning($"Failed to handle {msg}"); - return false; + return result; } - bool result; LogLevel logLevel; if (response.Result.IsEqualTo("true")) { logLevel = LogLevel.Information; - result = true; + result.Success = true; + result.IsValid = true; } else { logLevel = LogLevel.Warning; - result = false; } _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); @@ -111,16 +114,16 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, Criteri catch (Exception ex) { _logger.LogError(ex, $"Error when handling {msg}"); - return false; + return result; } } - private List BuildArguments(string? name, JsonDocument? args) + private List BuildArguments(string? name, JsonElement? args) { var keyValues = new List(); if (args != null) { - keyValues.Add(new KeyValue(name ?? "trigger_args", args.RootElement.GetRawText())); + keyValues.Add(new KeyValue(name ?? "trigger_args", args.Value.GetRawText())); } return keyValues; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs index 19982448e..2d1dc6844 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -20,6 +20,7 @@ global using BotSharp.Abstraction.Rules; global using BotSharp.Abstraction.Rules.Options; global using BotSharp.Abstraction.Rules.Models; +global using BotSharp.Abstraction.Rules.Hooks; global using BotSharp.Abstraction.Utilities; global using BotSharp.Abstraction.Coding; global using BotSharp.Abstraction.Coding.Contexts; @@ -28,3 +29,5 @@ global using BotSharp.Abstraction.Coding.Utils; global using BotSharp.Abstraction.Coding.Settings; global using BotSharp.Abstraction.Hooks; + +global using BotSharp.Core.Rules.Constants; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index ce25d121f..a2cfb4e3e 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using System; using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -9,7 +10,8 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public string Criteria { get; set; } = default!; - public AgentRuleActionMongModel? Action { get; set; } + public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } + public AgentRuleActionMongoModel? RuleAction { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -18,7 +20,8 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = AgentRuleActionMongModel.ToMongoModel(rule.Action) + RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria), + RuleAction = AgentRuleActionMongoModel.ToMongoModel(rule.RuleAction) }; } @@ -29,26 +32,57 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, Criteria = rule.Criteria, - Action = AgentRuleActionMongModel.ToDomainModel(rule.Action) + RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria), + RuleAction = AgentRuleActionMongoModel.ToDomainModel(rule.RuleAction) }; } } [BsonIgnoreExtraElements(Inherited = true)] -public class AgentRuleActionMongModel +public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel { - public string Name { get; set; } - public bool Disabled { get; set; } - public BsonDocument? Config { get; set; } + public static AgentRuleCriteriaMongoModel? ToMongoModel(AgentRuleCriteria? criteria) + { + if (criteria == null) + { + return null; + } + + return new AgentRuleCriteriaMongoModel + { + Name = criteria.Name, + Disabled = criteria.Disabled, + Config = criteria.Config != null ? BsonDocument.Parse(criteria.Config.RootElement.GetRawText()) : null + }; + } + + public static AgentRuleCriteria? ToDomainModel(AgentRuleCriteriaMongoModel? criteria) + { + if (criteria == null) + { + return null; + } + + return new AgentRuleCriteria + { + Name = criteria.Name, + Disabled = criteria.Disabled, + Config = criteria.Config != null ? JsonDocument.Parse(criteria.Config.ToJson()) : null + }; + } +} - public static AgentRuleActionMongModel? ToMongoModel(AgentRuleAction? action) +[BsonIgnoreExtraElements(Inherited = true)] +public class AgentRuleActionMongoModel : AgentRuleConfigMongoModel +{ + public static AgentRuleActionMongoModel? ToMongoModel(AgentRuleAction? action) { if (action == null) { return null; } - return new AgentRuleActionMongModel + return new AgentRuleActionMongoModel { Name = action.Name, Disabled = action.Disabled, @@ -56,7 +90,7 @@ public class AgentRuleActionMongModel }; } - public static AgentRuleAction? ToDomainModel(AgentRuleActionMongModel? action) + public static AgentRuleAction? ToDomainModel(AgentRuleActionMongoModel? action) { if (action == null) { @@ -70,4 +104,12 @@ public class AgentRuleActionMongModel Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null }; } +} + +[BsonIgnoreExtraElements(Inherited = true)] +public class AgentRuleConfigMongoModel +{ + public string Name { get; set; } + public bool Disabled { get; set; } + public BsonDocument? Config { get; set; } } \ No newline at end of file From a318c7a167299c846d844370a463333761ca1730 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 28 Jan 2026 14:36:36 -0600 Subject: [PATCH 31/91] refine agent rule structure --- .../Agents/Models/AgentRule.cs | 10 +++- .../Controllers/RuleController.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 56 +++++++++++-------- .../Controllers/Agent/AgentController.Rule.cs | 6 ++ .../Models/AgentRuleMongoElement.cs | 8 +-- 5 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 635b1628a..e0411801e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -10,9 +10,6 @@ public class AgentRule [JsonPropertyName("disabled")] public bool Disabled { get; set; } - [JsonPropertyName("criteria")] - public string Criteria { get; set; } = string.Empty; - [JsonPropertyName("rule_criteria")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentRuleCriteria? RuleCriteria { get; set; } @@ -24,6 +21,13 @@ public class AgentRule public class AgentRuleCriteria : AgentRuleConfigBase { + /// + /// Criteria + /// + [JsonPropertyName("criteria_text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string CriteriaText { get; set; } = string.Empty; + /// /// Adaptive configuration for rule criteria. /// This flexible JSON document can store any criteria-specific configuration. diff --git a/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs index ebb2bf0c5..feb314ef7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs @@ -22,7 +22,7 @@ public RuleController( _ruleEngine = ruleEngine; } - [HttpPost("/rule/trigger/run")] + [HttpPost("/rule/trigger/action")] public async Task RunAction([FromBody] RuleTriggerActionRequest request) { if (request == null) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 4241c220d..a078f3542 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -41,15 +41,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te } // Criteria validation - if (rule.RuleCriteria != null && !rule.RuleCriteria.Disabled) + if (!string.IsNullOrEmpty(rule.RuleCriteria?.Name) && !rule.RuleCriteria.Disabled) { - var criteriaContext = new RuleCriteriaContext - { - Text = text, - Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) - }; - var criteriaResult = await ExecuteCriteriaAsync(agent, trigger, rule.RuleCriteria?.Name, criteriaContext); - if (criteriaResult == null || !criteriaResult.IsValid) + var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states); + if (criteriaResult?.IsValid == false) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); continue; @@ -62,14 +57,8 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - var actionContext = new RuleActionContext - { - Text = text, - Parameters = BuildContextParameters(rule.RuleAction?.Config, states) - }; - - var action = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; - var actionResult = await ExecuteActionAsync(agent, trigger, action, actionContext); + var actionName = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; + var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states); if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { newConversationIds.Add(actionResult.ConversationId); @@ -81,12 +70,16 @@ public async Task> Triggered(IRuleTrigger trigger, string te #region Criteria - private async Task ExecuteCriteriaAsync( + private async Task ExecuteCriteriaAsync( Agent agent, + AgentRule rule, IRuleTrigger trigger, string? criteriaProvider, - RuleCriteriaContext context) + string text, + IEnumerable? states) { + var result = new RuleCriteriaResult(); + try { var criteria = _services.GetServices() @@ -94,9 +87,16 @@ public async Task> Triggered(IRuleTrigger trigger, string te if (criteria == null) { - return null; + return result; } + + var context = new RuleCriteriaContext + { + Text = text, + Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) + }; + _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", criteria.Provider, agent.Id, trigger.Name); @@ -108,7 +108,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute criteria context.Parameters ??= []; - var result = await criteria.ValidateAsync(agent, trigger, context); + result = await criteria.ValidateAsync(agent, trigger, context); foreach (var hook in hooks) { @@ -120,7 +120,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te catch (Exception ex) { _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", criteriaProvider ?? string.Empty, agent.Id); - return null; + return result; } } #endregion @@ -129,9 +129,11 @@ public async Task> Triggered(IRuleTrigger trigger, string te #region Action private async Task ExecuteActionAsync( Agent agent, + AgentRule rule, IRuleTrigger trigger, - string actionName, - RuleActionContext context) + string? actionName, + string text, + IEnumerable? states) { try { @@ -148,6 +150,12 @@ private async Task ExecuteActionAsync( return RuleActionResult.Failed(errorMsg); } + var context = new RuleActionContext + { + Text = text, + Parameters = BuildContextParameters(rule.RuleAction?.Config, states) + }; + _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", action.Name, agent.Id, trigger.Name); @@ -198,7 +206,7 @@ private async Task ExecuteActionAsync( return dict; } - private Dictionary ConvertToDictionary(JsonDocument doc) + private static Dictionary ConvertToDictionary(JsonDocument doc) { var dict = new Dictionary(); diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 0e4cc9dd0..366157418 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -17,6 +17,12 @@ public IEnumerable GetRuleTriggers() }).OrderBy(x => x.TriggerName); } + [HttpGet("/rule/criteria-providers")] + public async Task> GetRuleCriteriaProviders() + { + return _services.GetServices().Select(x => x.Provider).OrderBy(x => x); + } + [HttpGet("/rule/actions")] public async Task> GetRuleActions() { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index a2cfb4e3e..d031f4a96 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using System; using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -9,7 +8,6 @@ public class AgentRuleMongoElement { public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } - public string Criteria { get; set; } = default!; public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } public AgentRuleActionMongoModel? RuleAction { get; set; } @@ -19,7 +17,6 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria, RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria), RuleAction = AgentRuleActionMongoModel.ToMongoModel(rule.RuleAction) }; @@ -31,7 +28,6 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Criteria = rule.Criteria, RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria), RuleAction = AgentRuleActionMongoModel.ToDomainModel(rule.RuleAction) }; @@ -41,6 +37,8 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) [BsonIgnoreExtraElements(Inherited = true)] public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel { + public string CriteriaText { get; set; } + public static AgentRuleCriteriaMongoModel? ToMongoModel(AgentRuleCriteria? criteria) { if (criteria == null) @@ -51,6 +49,7 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel return new AgentRuleCriteriaMongoModel { Name = criteria.Name, + CriteriaText = criteria.CriteriaText, Disabled = criteria.Disabled, Config = criteria.Config != null ? BsonDocument.Parse(criteria.Config.RootElement.GetRawText()) : null }; @@ -66,6 +65,7 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel return new AgentRuleCriteria { Name = criteria.Name, + CriteriaText = criteria.CriteriaText, Disabled = criteria.Disabled, Config = criteria.Config != null ? JsonDocument.Parse(criteria.Config.ToJson()) : null }; From 953854a9f317c544dd0d3396c5d0fde512fa178d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 28 Jan 2026 16:10:18 -0600 Subject: [PATCH 32/91] add json options --- .../Rules/Models/RuleActionContext.cs | 3 +++ .../Rules/Models/RuleCriteriaContext.cs | 3 +++ .../Rules/Options/RuleTriggerOptions.cs | 6 ++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 16 ++++++++++------ .../Services/Actions/FunctionCallRuleAction.cs | 2 +- .../Services/Actions/HttpRuleAction.cs | 12 ++++++++++-- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index ca9c6045c..534130c63 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -1,7 +1,10 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs index d777db1d2..709e7be3e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -1,7 +1,10 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Rules.Models; public class RuleCriteriaContext { public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index a2ab78c85..abba98115 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Repositories.Filters; +using System.Text.Json; namespace BotSharp.Abstraction.Rules.Options; @@ -8,4 +9,9 @@ public class RuleTriggerOptions /// Filter agents /// public AgentFilter? AgentFilter { get; set; } + + /// + /// Json serializer options + /// + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index a078f3542..c72ad21bc 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -43,7 +43,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Criteria validation if (!string.IsNullOrEmpty(rule.RuleCriteria?.Name) && !rule.RuleCriteria.Disabled) { - var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states); + var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states, options); if (criteriaResult?.IsValid == false) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); @@ -58,7 +58,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var actionName = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; - var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states); + var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states, options); if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { newConversationIds.Add(actionResult.ConversationId); @@ -76,7 +76,8 @@ private async Task ExecuteCriteriaAsync( IRuleTrigger trigger, string? criteriaProvider, string text, - IEnumerable? states) + IEnumerable? states, + RuleTriggerOptions? triggerOptions) { var result = new RuleCriteriaResult(); @@ -94,7 +95,8 @@ private async Task ExecuteCriteriaAsync( var context = new RuleCriteriaContext { Text = text, - Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states) + Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states), + JsonOptions = triggerOptions?.JsonOptions }; _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", @@ -133,7 +135,8 @@ private async Task ExecuteActionAsync( IRuleTrigger trigger, string? actionName, string text, - IEnumerable? states) + IEnumerable? states, + RuleTriggerOptions? triggerOptions) { try { @@ -153,7 +156,8 @@ private async Task ExecuteActionAsync( var context = new RuleActionContext { Text = text, - Parameters = BuildContextParameters(rule.RuleAction?.Config, states) + Parameters = BuildContextParameters(rule.RuleAction?.Config, states), + JsonOptions = triggerOptions?.JsonOptions }; _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs index 6e01f71f4..811db8124 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs @@ -2,7 +2,7 @@ namespace BotSharp.Core.Rules.Services.Actions; -public class FunctionCallRuleAction : IRuleAction +public sealed class FunctionCallRuleAction : IRuleAction { private readonly IServiceProvider _services; private readonly ILogger _logger; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs index f1d2163ce..5e60e6c3f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs @@ -1,6 +1,6 @@ -using BotSharp.Abstraction.Options; using System.Net.Mime; using System.Text.Json; +using System.Text.Json.Serialization; using System.Web; namespace BotSharp.Core.Rules.Services.Actions; @@ -11,6 +11,14 @@ public sealed class HttpRuleAction : IRuleAction private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; + private readonly JsonSerializerOptions _defaultJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + AllowTrailingCommas = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + public HttpRuleAction( IServiceProvider services, ILogger logger, @@ -180,7 +188,7 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) return null; } - return JsonSerializer.Serialize(body, BotSharpOptions.defaultJsonOptions); + return JsonSerializer.Serialize(body, context.JsonOptions ?? _defaultJsonOptions); } } From 208b775b55e334dcefdd92c2637841acf9728230 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 29 Jan 2026 09:46:31 -0600 Subject: [PATCH 33/91] validate nullable rule action name --- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index c72ad21bc..bc6297e64 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -54,10 +54,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute action if (rule.RuleAction?.Disabled == true) { - continue; + continue; } - var actionName = rule?.RuleAction?.Name ?? RuleConstant.DEFAULT_ACTION_NAME; + var actionName = !string.IsNullOrEmpty(rule?.RuleAction?.Name) ? rule.RuleAction.Name : RuleConstant.DEFAULT_ACTION_NAME; var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states, options); if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) { From 2db1aa3963991280804186cee2d7770d6ac4a74c Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 29 Jan 2026 19:15:35 -0600 Subject: [PATCH 34/91] fix namespace and correct unit conversion --- .../Infrastructures/MessageQueues/IMQConsumer.cs | 2 -- .../Infrastructures/MessageQueues/MQConsumerBase.cs | 3 +-- .../Services/Actions/MessageQueueRuleAction.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs index 2f9296689..4df43dd0e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/IMQConsumer.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Infrastructures.MessageQueues.Models; - namespace BotSharp.Abstraction.Infrastructures.MessageQueues; /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs index 2fda58581..cd66be1fd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/MQConsumerBase.cs @@ -1,7 +1,6 @@ -using BotSharp.Abstraction.Infrastructures.MessageQueues; using Microsoft.Extensions.Logging; -namespace BotSharp.Plugin.RabbitMQ.Consumers; +namespace BotSharp.Abstraction.Infrastructures.MessageQueues; /// /// Abstract base class for RabbitMQ consumers. diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs index fcd406b55..1ee75b09e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs @@ -111,7 +111,7 @@ private long ParseDelay(RuleActionContext context) break; case "minute": case "minutes": - milliseconds = (long)TimeSpan.FromMilliseconds(qty).TotalMilliseconds; + milliseconds = (long)TimeSpan.FromMinutes(qty).TotalMilliseconds; break; case "hour": case "hours": From 30c670a32f8bf2af47faea1f5b50a740cc100b6b Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 29 Jan 2026 19:33:06 -0600 Subject: [PATCH 35/91] refine type conversion and mq channel --- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 11 +++-------- .../Services/RabbitMQService.cs | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index bc6297e64..53b748ea5 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -219,19 +219,14 @@ private async Task ExecuteActionAsync( dict[prop.Name] = prop.Value.ValueKind switch { JsonValueKind.String => prop.Value.GetString(), + JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, + JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, JsonValueKind.Number when prop.Value.TryGetInt32(out int intValue) => intValue, JsonValueKind.Number when prop.Value.TryGetInt64(out long longValue) => longValue, - JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, - JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, - JsonValueKind.Number when prop.Value.TryGetByte(out byte byteValue) => byteValue, - JsonValueKind.Number when prop.Value.TryGetSByte(out sbyte sbyteValue) => sbyteValue, - JsonValueKind.Number when prop.Value.TryGetUInt16(out ushort uint16Value) => uint16Value, - JsonValueKind.Number when prop.Value.TryGetUInt32(out uint uint32Value) => uint32Value, - JsonValueKind.Number when prop.Value.TryGetUInt64(out ulong uint64Value) => uint64Value, JsonValueKind.Number when prop.Value.TryGetDateTime(out DateTime dateTimeValue) => dateTimeValue, JsonValueKind.Number when prop.Value.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffsetValue) => dateTimeOffsetValue, JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue, - JsonValueKind.Number => prop.Value.GetRawText(), + JsonValueKind.Number => prop.Value.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.Null => null, diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 0ea2465a7..56774e885 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -155,7 +155,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); var isHandled = await registration.Consumer.HandleMessageAsync(config.QueueName, data); - if (!config.AutoAck && registration.Channel != null) + if (!config.AutoAck && registration.Channel?.IsOpen == true) { if (isHandled) { @@ -170,7 +170,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel catch (Exception ex) { _logger.LogError(ex, $"Error consuming message on queue '{config.QueueName}': {data}"); - if (!config.AutoAck && registration.Channel != null) + if (!config.AutoAck && registration.Channel?.IsOpen == true) { await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); } From 17b93b18f56fe4bdd969adf3ac67d056d7a96df0 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Thu, 29 Jan 2026 21:58:32 -0600 Subject: [PATCH 36/91] add json options in mq publish options --- .../MessageQueues/Models/MQPublishOptions.cs | 9 ++++++++- .../Services/Actions/MessageQueueRuleAction.cs | 7 ++++--- .../Models/RabbitMQConsumerConfig.cs | 5 ----- .../Services/RabbitMQService.cs | 12 ++++++------ 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs index b7c31d20e..dead523be 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/MessageQueues/Models/MQPublishOptions.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Infrastructures.MessageQueues.Models; /// @@ -29,5 +31,10 @@ public class MQPublishOptions /// /// Additional arguments for the publish configuration (MQ-specific). /// - public Dictionary Arguments { get; set; } = new(); + public Dictionary Arguments { get; set; } = []; + + /// + /// Json serializer options + /// + public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs index 1ee75b09e..2f819c6f5 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs @@ -45,7 +45,7 @@ public async Task ExecuteAsync( }; // Publish message to queue - var mqOptions = GetMQPublishOptions(context); + var mqOptions = BuildMQPublishOptions(context); var success = await mqService.PublishAsync(payload, mqOptions); if (success) @@ -72,7 +72,7 @@ public async Task ExecuteAsync( } } - private MQPublishOptions GetMQPublishOptions(RuleActionContext context) + private MQPublishOptions BuildMQPublishOptions(RuleActionContext context) { var topicName = context.Parameters.TryGetValueOrDefault("mq_topic_name", string.Empty); var routingKey = context.Parameters.TryGetValueOrDefault("mq_routing_key", string.Empty); @@ -82,7 +82,8 @@ private MQPublishOptions GetMQPublishOptions(RuleActionContext context) { TopicName = topicName, RoutingKey = routingKey, - DelayMilliseconds = delayMilliseconds + DelayMilliseconds = delayMilliseconds, + JsonOptions = context.JsonOptions }; } diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs index 4dc8f8ed5..93754d455 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Models/RabbitMQConsumerConfig.cs @@ -17,11 +17,6 @@ internal class RabbitMQConsumerConfig /// internal string RoutingKey { get; set; } = "rabbitmq.routing"; - /// - /// Whether to automatically acknowledge messages. - /// - internal bool AutoAck { get; set; } = false; - /// /// Additional arguments for the consumer configuration. /// diff --git a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs index 56774e885..0117bad14 100644 --- a/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs +++ b/src/Plugins/BotSharp.Plugin.RabbitMQ/Services/RabbitMQService.cs @@ -86,7 +86,7 @@ public async Task UnsubscribeAsync(string key) await channel.BasicConsumeAsync( queue: config.QueueName, - autoAck: config.AutoAck, + autoAck: false, consumer: asyncConsumer); _logger.LogWarning($"RabbitMQ consuming queue '{config.QueueName}'."); @@ -155,7 +155,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel _logger.LogInformation($"Message received on '{config.QueueName}', id: {eventArgs.BasicProperties?.MessageId}, data: {data}"); var isHandled = await registration.Consumer.HandleMessageAsync(config.QueueName, data); - if (!config.AutoAck && registration.Channel?.IsOpen == true) + if (registration.Channel?.IsOpen == true) { if (isHandled) { @@ -170,7 +170,7 @@ private async Task ConsumeEventAsync(ConsumerRegistration registration, BasicDel catch (Exception ex) { _logger.LogError(ex, $"Error consuming message on queue '{config.QueueName}': {data}"); - if (!config.AutoAck && registration.Channel?.IsOpen == true) + if (registration.Channel?.IsOpen == true) { await registration.Channel.BasicNackAsync(eventArgs.DeliveryTag, multiple: false, requeue: false); } @@ -222,7 +222,7 @@ await channel.ExchangeDeclareAsync( var messageId = options.MessageId ?? Guid.NewGuid().ToString(); var message = new MQMessage(payload, messageId); - var body = ConvertToBinary(message); + var body = ConvertToBinary(message, options.JsonOptions); var properties = new BasicProperties { MessageId = messageId, @@ -272,9 +272,9 @@ private RetryPolicy BuildRetryPolicy() }); } - private byte[] ConvertToBinary(T data) + private static byte[] ConvertToBinary(T data, JsonSerializerOptions? jsonOptions = null) { - var jsonStr = JsonSerializer.Serialize(data); + var jsonStr = JsonSerializer.Serialize(data, jsonOptions); var body = Encoding.UTF8.GetBytes(jsonStr); return body; } From f645b0e401f476a7ed6ecda6f49e2122f04ec96a Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 9 Feb 2026 17:52:44 -0600 Subject: [PATCH 37/91] relocate --- .../{Services => }/Actions/ChatRuleAction.cs | 2 +- .../{Services => }/Actions/FunctionCallRuleAction.cs | 2 +- .../{Services => }/Actions/HttpRuleAction.cs | 2 +- .../{Services => }/Actions/MessageQueueRuleAction.cs | 2 +- .../{Services => }/Criteria/CodeScriptRuleCriteria.cs | 2 +- .../BotSharp.Core.Rules/DemoRuleTrigger.cs | 10 ++++++++++ src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs | 6 ++++-- 7 files changed, 19 insertions(+), 7 deletions(-) rename src/Infrastructure/BotSharp.Core.Rules/{Services => }/Actions/ChatRuleAction.cs (97%) rename src/Infrastructure/BotSharp.Core.Rules/{Services => }/Actions/FunctionCallRuleAction.cs (96%) rename src/Infrastructure/BotSharp.Core.Rules/{Services => }/Actions/HttpRuleAction.cs (99%) rename src/Infrastructure/BotSharp.Core.Rules/{Services => }/Actions/MessageQueueRuleAction.cs (98%) rename src/Infrastructure/BotSharp.Core.Rules/{Services => }/Criteria/CodeScriptRuleCriteria.cs (98%) create mode 100644 src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs similarity index 97% rename from src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index 4c599a3e9..2bc3e60a3 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Core.Rules.Services.Actions; +namespace BotSharp.Core.Rules.Actions; public sealed class ChatRuleAction : IRuleAction { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs similarity index 96% rename from src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs index 811db8124..cae1d1afa 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs @@ -1,6 +1,6 @@ using BotSharp.Abstraction.Functions; -namespace BotSharp.Core.Rules.Services.Actions; +namespace BotSharp.Core.Rules.Actions; public sealed class FunctionCallRuleAction : IRuleAction { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs similarity index 99% rename from src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index 5e60e6c3f..cbeef118e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; using System.Web; -namespace BotSharp.Core.Rules.Services.Actions; +namespace BotSharp.Core.Rules.Actions; public sealed class HttpRuleAction : IRuleAction { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs similarity index 98% rename from src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs rename to src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs index 2f819c6f5..bf2029b1f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Actions/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs @@ -1,6 +1,6 @@ using BotSharp.Core.Rules.Models; -namespace BotSharp.Core.Rules.Services.Actions; +namespace BotSharp.Core.Rules.Actions; public sealed class MessageQueueRuleAction : IRuleAction { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs similarity index 98% rename from src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs rename to src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs index ad3489bc6..094bda40c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Services/Criteria/CodeScriptRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace BotSharp.Core.Rules.Services.Criteria; +namespace BotSharp.Core.Rules.Criteria; public class CodeScriptRuleCriteria : IRuleCriteria { diff --git a/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs b/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs new file mode 100644 index 000000000..68c4f1022 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Core.Rules; + +public class DemoRuleTrigger : IRuleTrigger +{ + public string Channel => "test"; + public string Name => nameof(DemoRuleTrigger); + + public string EntityType { get; set; } + public string EntityId { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 5b8ade6b0..d7e6df853 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,6 +1,6 @@ +using BotSharp.Core.Rules.Actions; +using BotSharp.Core.Rules.Criteria; using BotSharp.Core.Rules.Engines; -using BotSharp.Core.Rules.Services.Actions; -using BotSharp.Core.Rules.Services.Criteria; namespace BotSharp.Core.Rules; @@ -26,5 +26,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddScoped(); } } From 302092226353ae0902d8618e3e14a9cf7fa00fba Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 11 Feb 2026 15:51:17 -0600 Subject: [PATCH 38/91] minor fix --- .../BotSharp.Core.Rules/Actions/HttpRuleAction.cs | 4 ++-- .../BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index cbeef118e..ac278229c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -73,8 +73,8 @@ public async Task ExecuteAsync( if (response.IsSuccessStatusCode) { - _logger.LogInformation("HTTP rule action executed successfully for agent {AgentId}, Status: {StatusCode}", - agent.Id, response.StatusCode); + _logger.LogInformation("HTTP rule action executed successfully for agent {AgentId}, Status: {StatusCode}, Response: {Response}", + agent.Id, response.StatusCode, responseContent); return new RuleActionResult { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs index bf2029b1f..54cc081ba 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs @@ -89,7 +89,7 @@ private MQPublishOptions BuildMQPublishOptions(RuleActionContext context) private long ParseDelay(RuleActionContext context) { - var qty = (double)context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0); + var qty = (double)context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0M); if (qty == 0) { qty = context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0.0); From dc950c02ca4b38fd84a3fcbb94a8f3a21926e116 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 11 Feb 2026 16:06:00 -0600 Subject: [PATCH 39/91] rename to http_request_headers --- .../BotSharp.Core.Rules/Actions/HttpRuleAction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index ac278229c..dbcf8b525 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -170,7 +170,7 @@ private string BuildUrl(RuleActionContext context) private void AddHttpHeaders(HttpClient client, RuleActionContext context) { - var headerParams = context.Parameters.TryGetValueOrDefault>("http_headers"); + var headerParams = context.Parameters.TryGetValueOrDefault>("http_request_headers"); if (!headerParams.IsNullOrEmpty()) { foreach (var header in headerParams!) From 40600116f133ff6b05b25acbf4ea5091e42182af Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 11 Feb 2026 16:07:12 -0600 Subject: [PATCH 40/91] minor change --- src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs | 6 +++--- src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs b/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs index 68c4f1022..70a9eac22 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/DemoRuleTrigger.cs @@ -2,9 +2,9 @@ namespace BotSharp.Core.Rules; public class DemoRuleTrigger : IRuleTrigger { - public string Channel => "test"; + public string Channel => "crontab"; public string Name => nameof(DemoRuleTrigger); - public string EntityType { get; set; } - public string EntityId { get; set; } + public string EntityType { get; set; } = "DemoType"; + public string EntityId { get; set; } = "DemoId"; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index d7e6df853..afbb28e6a 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -27,6 +27,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); +#if DEBUG services.AddScoped(); +#endif } } From 030dee787fb60b80081f4e5e46b6e2ff21b8500d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 11 Feb 2026 16:59:42 -0600 Subject: [PATCH 41/91] extend to actions --- .../Agents/Models/AgentRule.cs | 4 +- .../Rules/Hooks/IRuleTriggerHook.cs | 4 +- .../Actions/ChatRuleAction.cs | 2 +- .../Actions/FunctionCallRuleAction.cs | 2 +- .../Actions/HttpRuleAction.cs | 3 +- .../Actions/MessageQueueRuleAction.cs | 129 ------------------ .../Constants/RuleConstant.cs | 1 - .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 36 ++--- .../BotSharp.Core.Rules/RulesPlugin.cs | 1 - .../Models/AgentRuleMongoElement.cs | 14 +- 10 files changed, 34 insertions(+), 162 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index e0411801e..3a642e834 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -14,9 +14,9 @@ public class AgentRule [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentRuleCriteria? RuleCriteria { get; set; } - [JsonPropertyName("rule_action")] + [JsonPropertyName("rule_actions")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public AgentRuleAction? RuleAction { get; set; } + public IEnumerable RuleActions { get; set; } = []; } public class AgentRuleCriteria : AgentRuleConfigBase diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 9e9817890..22fd031bf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -8,6 +8,6 @@ public interface IRuleTriggerHook : IHookBase Task BeforeRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; Task AfterRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; - Task BeforeRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; - Task AfterRuleActionExecuted(Agent agent, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index 2bc3e60a3..eec1db7f7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -13,7 +13,7 @@ public ChatRuleAction( _logger = logger; } - public string Name => RuleConstant.DEFAULT_ACTION_NAME; + public string Name => "send_message_to_agent"; public async Task ExecuteAsync( Agent agent, diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs index cae1d1afa..b5beec87c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs @@ -15,7 +15,7 @@ public FunctionCallRuleAction( _logger = logger; } - public string Name => "BotSharp-function-call"; + public string Name => "function_call"; public async Task ExecuteAsync( Agent agent, diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index dbcf8b525..ddce95080 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -29,7 +29,7 @@ public HttpRuleAction( _httpClientFactory = httpClientFactory; } - public string Name => "BotSharp-http"; + public string Name => "http_request"; public async Task ExecuteAsync( Agent agent, @@ -134,6 +134,7 @@ private string BuildUrl(RuleActionContext context) url = builder.ToString(); } + _logger.LogInformation("HTTP url after filling: {Url}", url); return url; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs deleted file mode 100644 index 54cc081ba..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/MessageQueueRuleAction.cs +++ /dev/null @@ -1,129 +0,0 @@ -using BotSharp.Core.Rules.Models; - -namespace BotSharp.Core.Rules.Actions; - -public sealed class MessageQueueRuleAction : IRuleAction -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - - public MessageQueueRuleAction( - IServiceProvider services, - ILogger logger) - { - _services = services; - _logger = logger; - } - - public string Name => "BotSharp-message-queue"; - - public async Task ExecuteAsync( - Agent agent, - IRuleTrigger trigger, - RuleActionContext context) - { - try - { - // Get message queue service - var mqService = _services.GetService(); - if (mqService == null) - { - var errorMsg = "Message queue service is not configured. Please ensure a message queue provider (e.g., RabbitMQ) is registered."; - _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); - } - - // Create message payload - var payload = new RuleMessagePayload - { - AgentId = agent.Id, - TriggerName = trigger.Name, - Channel = trigger.Channel, - Text = context.Text, - Timestamp = DateTime.UtcNow, - States = context.Parameters - }; - - // Publish message to queue - var mqOptions = BuildMQPublishOptions(context); - var success = await mqService.PublishAsync(payload, mqOptions); - - if (success) - { - _logger.LogInformation("MessageQueue rule action executed successfully for agent {AgentId}", agent.Id); - return new RuleActionResult - { - Success = true, - Response = $"Message published to queue: {mqOptions.TopicName}-{mqOptions.RoutingKey}" - }; - } - else - { - var errorMsg = $"Failed to publish message to queue {mqOptions.TopicName}-{mqOptions.RoutingKey}"; - _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error executing MessageQueue rule action for agent {AgentId} and trigger {TriggerName}", - agent.Id, trigger.Name); - return RuleActionResult.Failed(ex.Message); - } - } - - private MQPublishOptions BuildMQPublishOptions(RuleActionContext context) - { - var topicName = context.Parameters.TryGetValueOrDefault("mq_topic_name", string.Empty); - var routingKey = context.Parameters.TryGetValueOrDefault("mq_routing_key", string.Empty); - var delayMilliseconds = ParseDelay(context); - - return new MQPublishOptions - { - TopicName = topicName, - RoutingKey = routingKey, - DelayMilliseconds = delayMilliseconds, - JsonOptions = context.JsonOptions - }; - } - - private long ParseDelay(RuleActionContext context) - { - var qty = (double)context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0M); - if (qty == 0) - { - qty = context.Parameters.TryGetValueOrDefault("mq_delay_qty", 0.0); - } - - if (qty <= 0) - { - return 0L; - } - - var unit = context.Parameters.TryGetValueOrDefault("mq_delay_unit", string.Empty) ?? string.Empty; - unit = unit.ToLower(); - - var milliseconds = 0L; - switch (unit) - { - case "second": - case "seconds": - milliseconds = (long)TimeSpan.FromSeconds(qty).TotalMilliseconds; - break; - case "minute": - case "minutes": - milliseconds = (long)TimeSpan.FromMinutes(qty).TotalMilliseconds; - break; - case "hour": - case "hours": - milliseconds = (long)TimeSpan.FromHours(qty).TotalMilliseconds; - break; - case "day": - case "days": - milliseconds = (long)TimeSpan.FromDays(qty).TotalMilliseconds; - break; - } - - return milliseconds; - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index e9c180120..eca93d6c3 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -3,5 +3,4 @@ namespace BotSharp.Core.Rules.Constants; public static class RuleConstant { public const string DEFAULT_CRITERIA_PROVIDER = "BotSharp-code-script"; - public const string DEFAULT_ACTION_NAME = "BotSharp-chat"; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 53b748ea5..51a3aa71f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -50,18 +50,21 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } } - + // Execute action - if (rule.RuleAction?.Disabled == true) + var ruleActions = rule.RuleActions?.Where(x => x != null && !string.IsNullOrEmpty(x.Name) && !x.Disabled) ?? []; + if (ruleActions.IsNullOrEmpty()) { continue; } - var actionName = !string.IsNullOrEmpty(rule?.RuleAction?.Name) ? rule.RuleAction.Name : RuleConstant.DEFAULT_ACTION_NAME; - var actionResult = await ExecuteActionAsync(agent, rule, trigger, actionName, text, states, options); - if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) + foreach (var ruleAction in ruleActions) { - newConversationIds.Add(actionResult.ConversationId); + var actionResult = await ExecuteActionAsync(agent, ruleAction, trigger, text, states, options); + if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) + { + newConversationIds.Add(actionResult.ConversationId); + } } } @@ -131,9 +134,8 @@ private async Task ExecuteCriteriaAsync( #region Action private async Task ExecuteActionAsync( Agent agent, - AgentRule rule, + AgentRuleAction ruleAction, IRuleTrigger trigger, - string? actionName, string text, IEnumerable? states, RuleTriggerOptions? triggerOptions) @@ -144,11 +146,11 @@ private async Task ExecuteActionAsync( var actions = _services.GetServices(); // Find the matching action - var action = actions.FirstOrDefault(x => x.Name.IsEqualTo(actionName)); + var foundAction = actions.FirstOrDefault(x => x.Name.IsEqualTo(ruleAction.Name)); - if (action == null) + if (foundAction == null) { - var errorMsg = $"No rule action {actionName} is found"; + var errorMsg = $"No rule action {ruleAction.Name} is found"; _logger.LogWarning(errorMsg); return RuleActionResult.Failed(errorMsg); } @@ -156,33 +158,33 @@ private async Task ExecuteActionAsync( var context = new RuleActionContext { Text = text, - Parameters = BuildContextParameters(rule.RuleAction?.Config, states), + Parameters = BuildContextParameters(ruleAction.Config, states), JsonOptions = triggerOptions?.JsonOptions }; _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", - action.Name, agent.Id, trigger.Name); + foundAction.Name, agent.Id, trigger.Name); var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleActionExecuted(agent, trigger, context); + await hook.BeforeRuleActionExecuted(agent, ruleAction, trigger, context); } // Execute action context.Parameters ??= []; - var result = await action.ExecuteAsync(agent, trigger, context); + var result = await foundAction.ExecuteAsync(agent, trigger, context); foreach (var hook in hooks) { - await hook.AfterRuleActionExecuted(agent, trigger, result); + await hook.AfterRuleActionExecuted(agent, ruleAction, trigger, result); } return result; } catch (Exception ex) { - _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", actionName, agent.Id); + _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", ruleAction.Name, 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 afbb28e6a..c12ce1f3c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -25,7 +25,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); #if DEBUG services.AddScoped(); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index d031f4a96..4cf379d62 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -9,7 +9,7 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } - public AgentRuleActionMongoModel? RuleAction { get; set; } + public List RuleActions { get; set; } = []; public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -18,7 +18,7 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria), - RuleAction = AgentRuleActionMongoModel.ToMongoModel(rule.RuleAction) + RuleActions = rule.RuleActions?.Where(x => x != null).Select(x => AgentRuleActionMongoElement.ToMongoElement(x)!)?.ToList() ?? [] }; } @@ -29,7 +29,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) TriggerName = rule.TriggerName, Disabled = rule.Disabled, RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria), - RuleAction = AgentRuleActionMongoModel.ToDomainModel(rule.RuleAction) + RuleActions = rule.RuleActions?.Where(x => x != null).Select(x => AgentRuleActionMongoElement.ToDomainElement(x)!)?.ToList() ?? [] }; } } @@ -73,16 +73,16 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel } [BsonIgnoreExtraElements(Inherited = true)] -public class AgentRuleActionMongoModel : AgentRuleConfigMongoModel +public class AgentRuleActionMongoElement : AgentRuleConfigMongoModel { - public static AgentRuleActionMongoModel? ToMongoModel(AgentRuleAction? action) + public static AgentRuleActionMongoElement? ToMongoElement(AgentRuleAction? action) { if (action == null) { return null; } - return new AgentRuleActionMongoModel + return new AgentRuleActionMongoElement { Name = action.Name, Disabled = action.Disabled, @@ -90,7 +90,7 @@ public class AgentRuleActionMongoModel : AgentRuleConfigMongoModel }; } - public static AgentRuleAction? ToDomainModel(AgentRuleActionMongoModel? action) + public static AgentRuleAction? ToDomainElement(AgentRuleActionMongoElement? action) { if (action == null) { From b0dfa76ea2e459fa80a4f8bdf803269abdbaebf9 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 11 Feb 2026 17:10:34 -0600 Subject: [PATCH 42/91] pass rule criteria object --- .../Rules/Hooks/IRuleTriggerHook.cs | 4 ++-- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 22fd031bf..5d5c31fd6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -5,8 +5,8 @@ namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { - Task BeforeRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; - Task AfterRuleCriteriaExecuted(Agent agent, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; + Task BeforeRuleCriteriaExecuted(Agent agent, AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; + Task AfterRuleCriteriaExecuted(Agent agent, AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; Task BeforeRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; Task AfterRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 51a3aa71f..71d6dce90 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -43,7 +43,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Criteria validation if (!string.IsNullOrEmpty(rule.RuleCriteria?.Name) && !rule.RuleCriteria.Disabled) { - var criteriaResult = await ExecuteCriteriaAsync(agent, rule, trigger, rule.RuleCriteria?.Name, text, states, options); + var criteriaResult = await ExecuteCriteriaAsync(agent, rule.RuleCriteria, trigger, text, states, options); if (criteriaResult?.IsValid == false) { _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); @@ -75,9 +75,8 @@ public async Task> Triggered(IRuleTrigger trigger, string te #region Criteria private async Task ExecuteCriteriaAsync( Agent agent, - AgentRule rule, + AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, - string? criteriaProvider, string text, IEnumerable? states, RuleTriggerOptions? triggerOptions) @@ -87,7 +86,7 @@ private async Task ExecuteCriteriaAsync( try { var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == criteriaProvider); + .FirstOrDefault(x => x.Provider == ruleCriteria.Name); if (criteria == null) { @@ -98,7 +97,7 @@ private async Task ExecuteCriteriaAsync( var context = new RuleCriteriaContext { Text = text, - Parameters = BuildContextParameters(rule.RuleCriteria?.Config, states), + Parameters = BuildContextParameters(ruleCriteria.Config, states), JsonOptions = triggerOptions?.JsonOptions }; @@ -108,7 +107,7 @@ private async Task ExecuteCriteriaAsync( var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleCriteriaExecuted(agent, trigger, context); + await hook.BeforeRuleCriteriaExecuted(agent, ruleCriteria, trigger, context); } // Execute criteria @@ -117,14 +116,14 @@ private async Task ExecuteCriteriaAsync( foreach (var hook in hooks) { - await hook.AfterRuleCriteriaExecuted(agent, trigger, result); + await hook.AfterRuleCriteriaExecuted(agent, ruleCriteria, trigger, result); } return result; } catch (Exception ex) { - _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", criteriaProvider ?? string.Empty, agent.Id); + _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", ruleCriteria.Name, agent.Id); return result; } } From 717ec7a7845208b20493c258ca77249b86dd3560 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 11 Feb 2026 17:37:28 -0600 Subject: [PATCH 43/91] add action step result --- .../Rules/Models/RuleActionContext.cs | 1 + .../Rules/Models/RuleActionResult.cs | 12 ++++++--- .../Actions/ChatRuleAction.cs | 6 ++++- .../Actions/FunctionCallRuleAction.cs | 8 +++++- .../Actions/HttpRuleAction.cs | 6 ++++- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 27 ++++++++++++++++--- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 534130c63..8be9d934e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -6,5 +6,6 @@ public class RuleActionContext { public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; + public IEnumerable PrevStepResults { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs index ffdaeab2e..5bc613227 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs @@ -11,14 +11,14 @@ public class RuleActionResult public bool Success { get; set; } /// - /// The conversation ID if a new conversation was created + /// Response content from the action /// - public string? ConversationId { get; set; } + public string? Response { get; set; } /// - /// Response content from the action + /// Result data /// - public string? Response { get; set; } + public Dictionary Data { get; set; } = []; /// /// Error message if the action failed @@ -44,3 +44,7 @@ public static RuleActionResult Failed(string errorMessage) } } +public class RuleActionStepResult : RuleActionResult +{ + public AgentRuleAction RuleAction { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index eec1db7f7..12d50a080 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -60,7 +60,11 @@ await convService.SendMessage(agent.Id, return new RuleActionResult { Success = true, - ConversationId = conv.Id + Data = new() + { + ["agent_id"] = agent.Id, + ["conversation_id"] = conv.Id + } }; } catch (Exception ex) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs index b5beec87c..21e3387ec 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs @@ -38,7 +38,13 @@ public async Task ExecuteAsync( return new RuleActionResult { Success = true, - Response = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content + Response = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content, + Data = new() + { + ["function_name"] = funcName!, + ["function_argument"] = funcArg!, + ["function_call_result"] = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content ?? string.Empty + } }; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index ddce95080..5b0826524 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -79,7 +79,11 @@ public async Task ExecuteAsync( return new RuleActionResult { Success = true, - Response = responseContent + Response = responseContent, + Data = new() + { + ["http_response"] = responseContent + } }; } else diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 71d6dce90..ad907b01f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -58,12 +58,29 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } + var stepResults = new List(); foreach (var ruleAction in ruleActions) { - var actionResult = await ExecuteActionAsync(agent, ruleAction, trigger, text, states, options); - if (actionResult?.Success == true && !string.IsNullOrEmpty(actionResult.ConversationId)) + var actionResult = await ExecuteActionAsync(agent, ruleAction, trigger, text, states, options, stepResults); + if (actionResult == null) { - newConversationIds.Add(actionResult.ConversationId); + continue; + } + + stepResults.Add(new() + { + RuleAction = ruleAction, + Success = actionResult.Success, + Response = actionResult.Response, + ErrorMessage = actionResult.ErrorMessage, + Data = actionResult.Data + }); + + if (actionResult?.Success == true + && actionResult.Data.TryGetValue("conversation_id", out var convId) + && convId != null) + { + newConversationIds.Add(convId.ToString()!); } } } @@ -137,7 +154,8 @@ private async Task ExecuteActionAsync( IRuleTrigger trigger, string text, IEnumerable? states, - RuleTriggerOptions? triggerOptions) + RuleTriggerOptions? triggerOptions, + IEnumerable prevStepResults) { try { @@ -158,6 +176,7 @@ private async Task ExecuteActionAsync( { Text = text, Parameters = BuildContextParameters(ruleAction.Config, states), + PrevStepResults = prevStepResults, JsonOptions = triggerOptions?.JsonOptions }; From e63e874205ba615adaae32307f6b3c757d2af476 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 17 Feb 2026 11:39:47 -0600 Subject: [PATCH 44/91] minor change criteria provider --- .../BotSharp.Core.Rules/Constants/RuleConstant.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index eca93d6c3..daf54db77 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -2,5 +2,5 @@ namespace BotSharp.Core.Rules.Constants; public static class RuleConstant { - public const string DEFAULT_CRITERIA_PROVIDER = "BotSharp-code-script"; + public const string DEFAULT_CRITERIA_PROVIDER = "code_script"; } From b706f14fdcca86dfb818f0da93c6005beb7b5cbd Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 17 Feb 2026 22:02:23 -0600 Subject: [PATCH 45/91] use string parameter --- .../Agents/Models/AgentRule.cs | 3 + .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 10 ++ .../Rules/Models/RuleActionContext.cs | 3 +- .../Rules/Models/RuleActionResult.cs | 7 +- .../Rules/Models/RuleCriteriaContext.cs | 2 +- .../Options/RuleExecutionActionOptions.cs | 8 ++ .../Utilities/ObjectExtensions.cs | 28 ++++++ .../Actions/ChatRuleAction.cs | 2 +- .../Actions/FunctionCallRuleAction.cs | 6 +- .../Actions/HttpRuleAction.cs | 23 +---- .../Criteria/CodeScriptRuleCriteria.cs | 8 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 99 +++++++++++++++---- 12 files changed, 153 insertions(+), 46 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 3a642e834..58e5a9fa9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -64,4 +64,7 @@ public class AgentRuleConfigBase [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public virtual JsonDocument? Config { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public virtual string? JsonConfig { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index c7a6d847b..4638950d8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -13,4 +13,14 @@ public interface IRuleEngine /// Task> Triggered(IRuleTrigger trigger, string text, IEnumerable? states = null, RuleTriggerOptions? options = null) => throw new NotImplementedException(); + + /// + /// Execute rule actions + /// + /// + /// + /// + /// + Task RunActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) + => Task.FromResult(false); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 8be9d934e..aecbf2eed 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -5,7 +5,8 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleActionContext { public string Text { get; set; } = string.Empty; - public Dictionary Parameters { get; set; } = []; + public Dictionary Parameters { get; set; } = []; public IEnumerable PrevStepResults { get; set; } = []; + public IEnumerable NextActions { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs index 5bc613227..453d75a4c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs @@ -18,13 +18,18 @@ public class RuleActionResult /// /// Result data /// - public Dictionary Data { get; set; } = []; + public Dictionary Data { get; set; } = []; /// /// Error message if the action failed /// public string? ErrorMessage { get; set; } + /// + /// Whether the action is delayed + /// + public bool IsDelayed { get; set; } + public static RuleActionResult Succeeded(string? response = null) { return new RuleActionResult diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs index 709e7be3e..7fff33344 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -5,6 +5,6 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleCriteriaContext { public string Text { get; set; } = string.Empty; - public Dictionary Parameters { get; set; } = []; + public Dictionary Parameters { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs new file mode 100644 index 000000000..48787e06e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleExecutionActionOptions +{ + public string AgentId { get; set; } + public string Text { get; set; } + public IEnumerable States { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs index 9efdf6f42..c0f858fd5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/ObjectExtensions.cs @@ -103,4 +103,32 @@ public static bool TryGetValue(this IDictionary dict, string return false; } + + + public static T? TryGetObjectValueOrDefault(this IDictionary dict, string key, T? defaultValue = default, JsonSerializerOptions? jsonOptions = null) where T : class + { + return dict.TryGetObjectValue(key, out var value, jsonOptions) + ? value! + : defaultValue; + } + + public static bool TryGetObjectValue(this IDictionary dict, string key, out T? result, JsonSerializerOptions? jsonOptions = null) where T : class + { + result = default; + + if (!dict.TryGetValue(key, out var value) || value is null) + { + return false; + } + + try + { + result = JsonSerializer.Deserialize(value, jsonOptions); + return true; + } + catch + { + return false; + } + } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index 12d50a080..5b8484612 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -43,7 +43,7 @@ public async Task ExecuteAsync( if (!context.Parameters.IsNullOrEmpty()) { - var states = context.Parameters.Select(x => new MessageState(x.Key, x.Value)); + var states = context.Parameters.Where(x => x.Value != null).Select(x => new MessageState(x.Key, x.Value!)); allStates.AddRange(states); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs index 21e3387ec..7251a24d7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs @@ -22,7 +22,7 @@ public async Task ExecuteAsync( IRuleTrigger trigger, RuleActionContext context) { - var funcName = context.Parameters.TryGetValueOrDefault("function_name", string.Empty); + var funcName = context.Parameters.TryGetValue("function_name", out var fName) ? fName : null; var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); if (func == null) @@ -32,7 +32,7 @@ public async Task ExecuteAsync( return RuleActionResult.Failed(errorMsg); } - var funcArg = context.Parameters.TryGetValueOrDefault("function_argument") ?? new(); + var funcArg = context.Parameters.TryGetObjectValueOrDefault("function_argument", new()) ?? new(); await func.Execute(funcArg); return new RuleActionResult @@ -42,7 +42,7 @@ public async Task ExecuteAsync( Data = new() { ["function_name"] = funcName!, - ["function_argument"] = funcArg!, + ["function_argument"] = funcArg?.ConvertToString() ?? "{}", ["function_call_result"] = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content ?? string.Empty } }; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index 5b0826524..2c85c3560 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -11,14 +11,6 @@ public sealed class HttpRuleAction : IRuleAction private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; - private readonly JsonSerializerOptions _defaultJsonOptions = new() - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - AllowTrailingCommas = true, - ReferenceHandler = ReferenceHandler.IgnoreCycles - }; - public HttpRuleAction( IServiceProvider services, ILogger logger, @@ -103,7 +95,7 @@ public async Task ExecuteAsync( private string BuildUrl(RuleActionContext context) { - var url = context.Parameters.TryGetValueOrDefault("http_url"); + var url = context.Parameters.GetValueOrDefault("http_url", string.Empty); if (string.IsNullOrEmpty(url)) { throw new ArgumentNullException("Unable to find http_url in context"); @@ -121,7 +113,7 @@ private string BuildUrl(RuleActionContext context) } // Add query parameters - var queryParams = context.Parameters.TryGetValueOrDefault>("http_query_params"); + var queryParams = context.Parameters.TryGetObjectValueOrDefault>("http_query_params"); if (!queryParams.IsNullOrEmpty()) { var builder = new UriBuilder(url); @@ -144,7 +136,7 @@ private string BuildUrl(RuleActionContext context) private HttpMethod? GetHttpMethod(RuleActionContext context) { - var method = context.Parameters.TryGetValueOrDefault("http_method", string.Empty); + var method = context.Parameters.GetValueOrDefault("http_method", string.Empty); var innerMethod = method?.Trim()?.ToUpper(); HttpMethod? matchMethod = null; @@ -175,7 +167,7 @@ private string BuildUrl(RuleActionContext context) private void AddHttpHeaders(HttpClient client, RuleActionContext context) { - var headerParams = context.Parameters.TryGetValueOrDefault>("http_request_headers"); + var headerParams = context.Parameters.TryGetObjectValueOrDefault>("http_request_headers"); if (!headerParams.IsNullOrEmpty()) { foreach (var header in headerParams!) @@ -188,12 +180,7 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) private string? GetHttpRequestBody(RuleActionContext context) { var body = context.Parameters.GetValueOrDefault("http_request_body"); - if (body == null) - { - return null; - } - - return JsonSerializer.Serialize(body, context.JsonOptions ?? _defaultJsonOptions); + return body; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs index 094bda40c..f4bd9e04c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs @@ -29,7 +29,7 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger tr return result; } - var provider = context.Parameters.TryGetValueOrDefault("code_processor") ?? BuiltInCodeProcessor.PyInterpreter; + var provider = context.Parameters.GetValueOrDefault("code_processor", BuiltInCodeProcessor.PyInterpreter); var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); if (processor == null) { @@ -38,7 +38,7 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger tr } var agentService = _services.GetRequiredService(); - var scriptName = context.Parameters.TryGetValueOrDefault("code_script_name") ?? $"{trigger.Name}_rule.py"; + var scriptName = context.Parameters.GetValueOrDefault("code_script_name", $"{trigger.Name}_rule.py"); var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name})."; @@ -53,8 +53,8 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger tr { var hooks = _services.GetHooks(agent.Id); - var argName = context.Parameters.TryGetValueOrDefault("argument_name"); - var argValue = context.Parameters.TryGetValueOrDefault("argument_value"); + var argName = context.Parameters.GetValueOrDefault("argument_name", null); + var argValue = context.Parameters.TryGetValue("argument_value", out var val) && val != null ? JsonSerializer.Deserialize(val) : (JsonElement?)null; var arguments = BuildArguments(argName, argValue); var codeExeContext = new CodeExecutionContext { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index ad907b01f..e91a664b0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -59,14 +59,20 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var stepResults = new List(); - foreach (var ruleAction in ruleActions) + for (int i = 0; i < ruleActions.Count(); i++) { - var actionResult = await ExecuteActionAsync(agent, ruleAction, trigger, text, states, options, stepResults); + var ruleAction = ruleActions.ElementAt(i); + var actionResult = await ExecuteActionAsync(agent, ruleAction, ruleActions.Skip(i + 1), trigger, text, states, stepResults, options); if (actionResult == null) { continue; } + if (!actionResult.Success) + { + break; + } + stepResults.Add(new() { RuleAction = ruleAction, @@ -82,12 +88,55 @@ public async Task> Triggered(IRuleTrigger trigger, string te { newConversationIds.Add(convId.ToString()!); } + + if (actionResult?.IsDelayed == true) + { + break; + } } } return newConversationIds; } + public async Task RunActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) + { + var agentService = _services.GetRequiredService(); + var agent = await agentService.GetAgent(options.AgentId); + + var stepResults = new List(); + for (int i = 0; i < actions.Count(); i++) + { + var ruleAction = actions.ElementAt(i); + var actionResult = await ExecuteActionAsync(agent, ruleAction, actions.Skip(i + 1), trigger, options.Text, options.States, stepResults); + if (actionResult == null) + { + continue; + } + + if (!actionResult.Success) + { + break; + } + + stepResults.Add(new() + { + RuleAction = ruleAction, + Success = actionResult.Success, + Response = actionResult.Response, + ErrorMessage = actionResult.ErrorMessage, + Data = actionResult.Data + }); + + if (actionResult?.IsDelayed == true) + { + break; + } + } + + return true; + } + #region Criteria private async Task ExecuteCriteriaAsync( @@ -150,12 +199,13 @@ private async Task ExecuteCriteriaAsync( #region Action private async Task ExecuteActionAsync( Agent agent, - AgentRuleAction ruleAction, + AgentRuleAction curRuleAction, + IEnumerable nextRuleActions, IRuleTrigger trigger, string text, IEnumerable? states, - RuleTriggerOptions? triggerOptions, - IEnumerable prevStepResults) + IEnumerable prevStepResults, + RuleTriggerOptions? triggerOptions = null) { try { @@ -163,11 +213,11 @@ private async Task ExecuteActionAsync( var actions = _services.GetServices(); // Find the matching action - var foundAction = actions.FirstOrDefault(x => x.Name.IsEqualTo(ruleAction.Name)); + var foundAction = actions.FirstOrDefault(x => x.Name.IsEqualTo(curRuleAction.Name)); if (foundAction == null) { - var errorMsg = $"No rule action {ruleAction.Name} is found"; + var errorMsg = $"No rule action {curRuleAction.Name} is found"; _logger.LogWarning(errorMsg); return RuleActionResult.Failed(errorMsg); } @@ -175,8 +225,9 @@ private async Task ExecuteActionAsync( var context = new RuleActionContext { Text = text, - Parameters = BuildContextParameters(ruleAction.Config, states), + Parameters = BuildContextParameters(curRuleAction.Config, states, prevStepResults), PrevStepResults = prevStepResults, + NextActions = nextRuleActions, JsonOptions = triggerOptions?.JsonOptions }; @@ -186,7 +237,7 @@ private async Task ExecuteActionAsync( var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleActionExecuted(agent, ruleAction, trigger, context); + await hook.BeforeRuleActionExecuted(agent, curRuleAction, trigger, context); } // Execute action @@ -195,14 +246,14 @@ private async Task ExecuteActionAsync( foreach (var hook in hooks) { - await hook.AfterRuleActionExecuted(agent, ruleAction, trigger, result); + await hook.AfterRuleActionExecuted(agent, curRuleAction, trigger, result); } return result; } catch (Exception ex) { - _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", ruleAction.Name, agent.Id); + _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", curRuleAction.Name, agent.Id); return RuleActionResult.Failed(ex.Message); } } @@ -210,9 +261,9 @@ private async Task ExecuteActionAsync( #region Private methods - private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states) + private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states, IEnumerable? stepResults = null) { - var dict = new Dictionary(); + var dict = new Dictionary(); if (config != null) { @@ -223,20 +274,33 @@ private async Task ExecuteActionAsync( { foreach (var state in states!) { - dict[state.Key] = state.Value; + dict[state.Key] = state.Value?.ConvertToString(); + } + } + + if (!stepResults.IsNullOrEmpty()) + { + foreach (var result in stepResults!) + { + if (result.Data.IsNullOrEmpty()) continue; + + foreach (var item in result.Data) + { + dict[item.Key] = item.Value; + } } } return dict; } - private static Dictionary ConvertToDictionary(JsonDocument doc) + private static Dictionary ConvertToDictionary(JsonDocument doc) { - var dict = new Dictionary(); + var dict = new Dictionary(); foreach (var prop in doc.RootElement.EnumerateObject()) { - dict[prop.Name] = prop.Value.ValueKind switch + object? value = prop.Value.ValueKind switch { JsonValueKind.String => prop.Value.GetString(), JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, @@ -255,6 +319,7 @@ JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue JsonValueKind.Object => prop.Value, _ => prop.Value }; + dict[prop.Name] = value?.ConvertToString(); } return dict; From f7dd5afd6cd789ea06640adf3648b2054d1acb70 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 18 Feb 2026 09:54:20 -0600 Subject: [PATCH 46/91] rename --- src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs | 2 +- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index 4638950d8..ff539e87f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -21,6 +21,6 @@ Task> Triggered(IRuleTrigger trigger, string text, IEnumerab /// /// /// - Task RunActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) + Task ExecuteActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) => Task.FromResult(false); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index e91a664b0..7cef06030 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -99,7 +99,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } - public async Task RunActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) + public async Task ExecuteActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) { var agentService = _services.GetRequiredService(); var agent = await agentService.GetAgent(options.AgentId); From 9b5a2194137e0da9da2097dc4a988def2c8a2753 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 18 Feb 2026 11:42:38 -0600 Subject: [PATCH 47/91] refine code hook --- .../Coding/Contexts/CodeExecutionContext.cs | 1 + .../Instructs/IInstructHook.cs | 2 +- .../Instructs/InstructHookBase.cs | 2 +- .../Criteria/CodeScriptRuleCriteria.cs | 16 +++++++++------- .../Services/InstructService.Execute.cs | 2 +- .../BotSharp.Logger/Hooks/InstructionLogHook.cs | 6 ++++-- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs index e2ec38a5a..8442eae45 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Contexts/CodeExecutionContext.cs @@ -4,4 +4,5 @@ public class CodeExecutionContext { public AgentCodeScript CodeScript { get; set; } public List Arguments { get; set; } = []; + public string? InvokeFrom { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs index 54fc80a90..a39e4bf05 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/IInstructHook.cs @@ -12,5 +12,5 @@ public interface IInstructHook : IHookBase Task OnResponseGenerated(InstructResponseModel response) => Task.CompletedTask; Task BeforeCodeExecution(Agent agent, CodeExecutionContext context) => Task.CompletedTask; - Task AfterCodeExecution(Agent agent, CodeExecutionResponseModel response) => Task.CompletedTask; + Task AfterCodeExecution(Agent agent, CodeExecutionContext context, CodeExecutionResponseModel response) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs index f5758434c..81023135f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Instructs/InstructHookBase.cs @@ -28,7 +28,7 @@ public virtual async Task BeforeCodeExecution(Agent agent, CodeExecutionContext await Task.CompletedTask; } - public virtual async Task AfterCodeExecution(Agent agent, CodeExecutionResponseModel response) + public virtual async Task AfterCodeExecution(Agent agent, CodeExecutionContext context, CodeExecutionResponseModel response) { await Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs index f4bd9e04c..45126f506 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs @@ -53,26 +53,28 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger tr { var hooks = _services.GetHooks(agent.Id); - var argName = context.Parameters.GetValueOrDefault("argument_name", null); - var argValue = context.Parameters.TryGetValue("argument_value", out var val) && val != null ? JsonSerializer.Deserialize(val) : (JsonElement?)null; + var argName = context.Parameters.GetValueOrDefault("code_script_arg_name", null); + var argValue = context.Parameters.TryGetValue("code_script_arg_value", out var val) && val != null ? JsonSerializer.Deserialize(val) : (JsonElement?)null; var arguments = BuildArguments(argName, argValue); - var codeExeContext = new CodeExecutionContext + var codeExecutionContext = new CodeExecutionContext { CodeScript = codeScript, - Arguments = arguments + Arguments = arguments, + InvokeFrom = nameof(CodeScriptRuleCriteria) }; foreach (var hook in hooks) { - await hook.BeforeCodeExecution(agent, codeExeContext); + await hook.BeforeCodeExecution(agent, codeExecutionContext); } + codeScript = codeExecutionContext.CodeScript; 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, + Arguments = codeExecutionContext.Arguments, UseLock = useLock, UseProcess = useProcess }, cancellationToken: cts.Token); @@ -87,7 +89,7 @@ public async Task ValidateAsync(Agent agent, IRuleTrigger tr foreach (var hook in hooks) { - await hook.AfterCodeExecution(agent, codeResponse); + await hook.AfterCodeExecution(agent, codeExecutionContext, codeResponse); } if (response == null || !response.Success) diff --git a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs index f0ec11c23..6995b49ec 100644 --- a/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs +++ b/src/Infrastructure/BotSharp.Core/Instructs/Services/InstructService.Execute.cs @@ -195,7 +195,7 @@ public async Task Execute( foreach (var hook in hooks) { await hook.AfterCompletion(agent, instructResult); - await hook.AfterCodeExecution(agent, codeExecution); + await hook.AfterCodeExecution(agent, context, codeExecution); } return instructResult; diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs index 6ef8c5935..117feaebe 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/InstructionLogHook.cs @@ -1,9 +1,11 @@ +using BotSharp.Abstraction.Coding.Contexts; using BotSharp.Abstraction.Coding.Models; using BotSharp.Abstraction.Instructs.Models; using BotSharp.Abstraction.Instructs.Settings; using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Users; using BotSharp.Abstraction.Utilities; +using System; namespace BotSharp.Logger.Hooks; @@ -61,7 +63,7 @@ await db.SaveInstructionLogs(new List await base.OnResponseGenerated(response); } - public override async Task AfterCodeExecution(Agent agent, CodeExecutionResponseModel response) + public override async Task AfterCodeExecution(Agent agent, CodeExecutionContext context, CodeExecutionResponseModel response) { if (response == null || !IsLoggingEnabled(agent?.Id)) { @@ -88,7 +90,7 @@ await db.SaveInstructionLogs(new List } }); - await base.AfterCodeExecution(agent, response); + await base.AfterCodeExecution(agent, context, response); } private bool IsLoggingEnabled(string? agentId) From 24d47b1de203e58df3099232dfd757a241d9952e Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 18 Feb 2026 11:44:10 -0600 Subject: [PATCH 48/91] add chat action response --- src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index 5b8484612..eae15d74e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -60,6 +60,7 @@ await convService.SendMessage(agent.Id, return new RuleActionResult { Success = true, + Response = conv.Id, Data = new() { ["agent_id"] = agent.Id, From 5fae93682fa2d48ea66a0129142fb8ebe8520685 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 19 Feb 2026 11:35:21 -0600 Subject: [PATCH 49/91] add skipping expression --- .../Agents/Models/AgentRule.cs | 10 ++- .../BotSharp.Abstraction/Rules/IRuleAction.cs | 9 ++- .../Rules/IRuleCriteria.cs | 3 + .../Actions/HttpRuleAction.cs | 7 +- .../Criteria/CodeScriptRuleCriteria.cs | 8 ++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 73 +++++++++++++++++-- .../Controllers/Agent/AgentController.Rule.cs | 16 +++- .../Models/AgentRuleMongoElement.cs | 8 +- 8 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 58e5a9fa9..2653feec2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -48,20 +48,22 @@ public class AgentRuleAction : AgentRuleConfigBase /// - For "MessageQueue" action: contains mq_config with topic_name, routing_key, etc. /// - For custom actions: can contain any custom configuration structure /// - [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public override JsonDocument? Config { get; set; } + + /// + /// Skipping the number of actions using liquid template, starting from the action itself. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SkippingExpression { get; set; } } public class AgentRuleConfigBase { - [JsonPropertyName("name")] public virtual string Name { get; set; } - [JsonPropertyName("disabled")] public virtual bool Disabled { get; set; } - [JsonPropertyName("config")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public virtual JsonDocument? Config { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index 9c2bf03d9..ee2d43e68 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -1,7 +1,5 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Rules.Models; -using BotSharp.Abstraction.Rules.Options; +using System.Text.Json; namespace BotSharp.Abstraction.Rules; @@ -15,6 +13,11 @@ public interface IRuleAction /// string Name { get; } + /// + /// The default config json format. + /// + JsonDocument DefaultConfig => JsonDocument.Parse("{}"); + /// /// Execute the rule action /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs index f94e0b36f..30658e582 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Rules.Models; +using System.Text.Json; namespace BotSharp.Abstraction.Rules; @@ -6,6 +7,8 @@ public interface IRuleCriteria { string Provider { get; } + JsonDocument DefaultConfig => JsonDocument.Parse("{}"); + Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) => Task.FromResult(new RuleCriteriaResult()); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index 2c85c3560..18aa2446a 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -1,6 +1,5 @@ using System.Net.Mime; using System.Text.Json; -using System.Text.Json.Serialization; using System.Web; namespace BotSharp.Core.Rules.Actions; @@ -23,6 +22,12 @@ public HttpRuleAction( public string Name => "http_request"; + public JsonDocument DefaultConfig => JsonDocument.Parse(JsonSerializer.Serialize(new + { + http_url = "https://dummy.example.com/api/v1/employees", + http_method = "GET" + })); + public async Task ExecuteAsync( Agent agent, IRuleTrigger trigger, diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs index 45126f506..31536844f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs @@ -20,6 +20,14 @@ public CodeScriptRuleCriteria( public string Provider => RuleConstant.DEFAULT_CRITERIA_PROVIDER; + public JsonDocument DefaultConfig => JsonDocument.Parse(JsonSerializer.Serialize(new + { + code_processor = BuiltInCodeProcessor.PyInterpreter, + code_script_name = "{trigger_name}_rule.py", + code_script_arg_name = "trigger_args", + code_script_arg_value = JsonDocument.Parse("{}") + })); + public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) { var result = new RuleCriteriaResult(); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 7cef06030..b1c200891 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Templating; using System.Data; using System.Text.Json; @@ -58,13 +59,24 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } + var actionIdx = 0; var stepResults = new List(); - for (int i = 0; i < ruleActions.Count(); i++) + while (actionIdx >= 0 && actionIdx < ruleActions.Count()) { - var ruleAction = ruleActions.ElementAt(i); - var actionResult = await ExecuteActionAsync(agent, ruleAction, ruleActions.Skip(i + 1), trigger, text, states, stepResults, options); + var ruleAction = ruleActions.ElementAt(actionIdx); + var dict = BuildContextParameters(ruleAction.Config, states, stepResults); + + var skipSteps = RenderSkippingExpression(ruleAction.SkippingExpression, dict); + if (skipSteps.HasValue && skipSteps > 0) + { + actionIdx += skipSteps.Value; + continue; + } + + var actionResult = await ExecuteActionAsync(agent, ruleAction, ruleActions.Skip(actionIdx + 1), trigger, text, dict, stepResults, options); if (actionResult == null) { + actionIdx++; continue; } @@ -93,6 +105,8 @@ public async Task> Triggered(IRuleTrigger trigger, string te { break; } + + actionIdx++; } } @@ -104,13 +118,24 @@ public async Task ExecuteActions(IRuleTrigger trigger, IEnumerable(); var agent = await agentService.GetAgent(options.AgentId); + var actionIdx = 0; var stepResults = new List(); - for (int i = 0; i < actions.Count(); i++) + while (actionIdx >= 0 && actionIdx < actions.Count()) { - var ruleAction = actions.ElementAt(i); - var actionResult = await ExecuteActionAsync(agent, ruleAction, actions.Skip(i + 1), trigger, options.Text, options.States, stepResults); + var ruleAction = actions.ElementAt(actionIdx); + var dict = BuildContextParameters(ruleAction.Config, options.States, stepResults); + + var skipSteps = RenderSkippingExpression(ruleAction.SkippingExpression, dict); + if (skipSteps.HasValue && skipSteps > 0) + { + actionIdx += skipSteps.Value; + continue; + } + + var actionResult = await ExecuteActionAsync(agent, ruleAction, actions.Skip(actionIdx + 1), trigger, options.Text, dict, stepResults); if (actionResult == null) { + actionIdx++; continue; } @@ -132,6 +157,8 @@ public async Task ExecuteActions(IRuleTrigger trigger, IEnumerable ExecuteActionAsync( IEnumerable nextRuleActions, IRuleTrigger trigger, string text, - IEnumerable? states, + Dictionary param, IEnumerable prevStepResults, RuleTriggerOptions? triggerOptions = null) { @@ -225,7 +252,7 @@ private async Task ExecuteActionAsync( var context = new RuleActionContext { Text = text, - Parameters = BuildContextParameters(curRuleAction.Config, states, prevStepResults), + Parameters = param, PrevStepResults = prevStepResults, NextActions = nextRuleActions, JsonOptions = triggerOptions?.JsonOptions @@ -325,4 +352,34 @@ JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue return dict; #endregion } + + private int? RenderSkippingExpression(string? expression, Dictionary dict) + { + int? steps = null; + + if (string.IsNullOrWhiteSpace(expression)) + { + return steps; + } + + var render = _services.GetRequiredService(); + var copy = dict != null + ? new Dictionary(dict.Where(x => x.Value != null).ToDictionary(x => x.Key, x => (object)x.Value!)) + : []; + var result = render.Render(expression, new Dictionary + { + { "states", copy } + }); + + if (int.TryParse(result, out var intVal)) + { + steps = intVal; + } + else if (bool.TryParse(result, out var boolVal)) + { + steps = boolVal ? 1 : 0; + } + + return steps; + } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 366157418..de5b21a14 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -18,14 +18,22 @@ public IEnumerable GetRuleTriggers() } [HttpGet("/rule/criteria-providers")] - public async Task> GetRuleCriteriaProviders() + public async Task> GetRuleCriteriaProviders() { - return _services.GetServices().Select(x => x.Provider).OrderBy(x => x); + return _services.GetServices().OrderBy(x => x.Provider).Select(x => new KeyValue + { + Key = x.Provider, + Value = x.DefaultConfig != null ? x.DefaultConfig.RootElement.GetRawText() : "{}" + }); } [HttpGet("/rule/actions")] - public async Task> GetRuleActions() + public async Task> GetRuleActions() { - return _services.GetServices().Select(x => x.Name).OrderBy(x => x); + return _services.GetServices().OrderBy(x => x.Name).Select(x => new KeyValue + { + Key = x.Name, + Value = x.DefaultConfig != null ? x.DefaultConfig.RootElement.GetRawText() : "{}" + }); } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 4cf379d62..1bd76635a 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -75,6 +75,8 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel [BsonIgnoreExtraElements(Inherited = true)] public class AgentRuleActionMongoElement : AgentRuleConfigMongoModel { + public string? SkippingExpression { get; set; } + public static AgentRuleActionMongoElement? ToMongoElement(AgentRuleAction? action) { if (action == null) @@ -86,7 +88,8 @@ public class AgentRuleActionMongoElement : AgentRuleConfigMongoModel { Name = action.Name, Disabled = action.Disabled, - Config = action.Config != null ? BsonDocument.Parse(action.Config.RootElement.GetRawText()) : null + Config = action.Config != null ? BsonDocument.Parse(action.Config.RootElement.GetRawText()) : null, + SkippingExpression = action.SkippingExpression }; } @@ -101,7 +104,8 @@ public class AgentRuleActionMongoElement : AgentRuleConfigMongoModel { Name = action.Name, Disabled = action.Disabled, - Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null + Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null, + SkippingExpression = action.SkippingExpression }; } } From 511c60f0688e42fbef8034e40b1c548e2bf58041 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 19 Feb 2026 12:12:55 -0600 Subject: [PATCH 50/91] minor change --- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index b1c200891..75a13d398 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -375,9 +375,9 @@ JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue { steps = intVal; } - else if (bool.TryParse(result, out var boolVal)) + else if (bool.TryParse(result, out var boolVal) && boolVal) { - steps = boolVal ? 1 : 0; + steps = 1; } return steps; From 20ce33539dc4b67a9fca3c71d2d0d965fbd34050 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 27 Feb 2026 15:49:03 -0600 Subject: [PATCH 51/91] use graph --- .../Agents/Models/RuleGraph.cs | 167 ++++++++++ .../Rules/Hooks/IRuleTriggerHook.cs | 3 + .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 5 + .../Rules/Models/RuleActionContext.cs | 2 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 308 +++++++++++++++--- 5 files changed, 433 insertions(+), 52 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs new file mode 100644 index 000000000..b4c750ed1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -0,0 +1,167 @@ +namespace BotSharp.Abstraction.Agents.Models; + +public class RuleGraph +{ + private List _nodes = []; + private List _edges = []; + + public RuleGraph() + { + _nodes = []; + _edges = []; + } + + public static RuleGraph Init() + { + return new RuleGraph(); + } + + public RuleNode? GetRootNode() + { + return _nodes.FirstOrDefault(x => x.Type.IsEqualTo("root")); + } + + public RuleNode? GetNode(string id) + { + return _nodes.FirstOrDefault(x => x.Id.IsEqualTo(id)); + } + + public IEnumerable GetNodes() + { + return [.. _nodes]; + } + + public IEnumerable GetEdges() + { + return [.. _edges]; + } + + public void SetNodes(IEnumerable nodes) + { + _nodes = [.. nodes?.ToList() ?? []]; + } + + public void SetEdges(IEnumerable edges) + { + _edges = [.. edges?.ToList() ?? []]; + } + + public void AddNode(RuleNode node) + { + var found = _nodes.Exists(x => x.Id.IsEqualTo(node.Id)); + if (!found) + { + _nodes.Add(node); + } + } + + public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) + { + var sourceFound = _nodes.Exists(x => x.Id.IsEqualTo(from.Id)); + var targetFound = _nodes.Exists(x => x.Id.IsEqualTo(to.Id)); + var edgeFound = _edges.Exists(x => x.Id.IsEqualTo(payload.Id)); + + if (!sourceFound) + { + _nodes.Add(from); + } + + if (!targetFound) + { + _nodes.Add(to); + } + + if (!edgeFound) + { + _edges.Add(new RuleEdge(from, to) + { + Id = payload.Id, + Name = payload.Name, + Type = payload.Type, + Config = payload.Config + }); + } + } + + public IEnumerable<(RuleNode, RuleEdge)> GetNeighbors(RuleNode node) + { + return _edges.Where(e => e.From != null && e.From.Id.IsEqualTo(node.Id)) + .Select(e => (e.To, e)); + } + + public RuleGraphInfo GetGraphInfo() + { + return new() + { + Nodes = _nodes, + Edges = _edges + }; + } + + public static RuleGraph FromGraphInfo(RuleGraphInfo graphInfo) + { + var graph = new RuleGraph(); + graph.SetNodes(graphInfo.Nodes); + graph.SetEdges(graphInfo.Edges); + return graph; + } +} + +public class RuleNode : GraphItemPayload +{ + /// + /// Node type: root, criteria, action, etc. + /// + public override string Type { get; set; } = "action"; + + public override string ToString() + { + return $"Node: {Name} ({Type})"; + } +} + +public class RuleEdge : GraphItemPayload +{ + /// + /// Edge type: is_next, etc. + /// + public override string Type { get; set; } = "is_next"; + + public RuleNode From { get; set; } + public RuleNode To { get; set; } + + public RuleEdge() + { + + } + + public RuleEdge(RuleNode from, RuleNode to) + { + From = from; + To = to; + } + + public override string ToString() + { + return $"Edge: {Name} ({Type}), Connects from Node ({From?.Name}) to Node ({To?.Name})"; + } +} + +public class GraphItemPayload +{ + public virtual string Id { get; set; } = Guid.NewGuid().ToString(); + public virtual string Name { get; set; } = null!; + public virtual string Type { get; set; } = "is_next"; + public virtual Dictionary Config { get; set; } = []; + + public GraphItemPayload() + { + + } +} + +public class RuleGraphInfo +{ + public IEnumerable Nodes { get; set; } = []; + public IEnumerable Edges { get; set; } = []; +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 5d5c31fd6..e70248cb3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -10,4 +10,7 @@ public interface IRuleTriggerHook : IHookBase Task BeforeRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; Task AfterRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; + + Task BeforeRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index ff539e87f..949b6dac9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules; public interface IRuleEngine @@ -23,4 +25,7 @@ Task> Triggered(IRuleTrigger trigger, string text, IEnumerab /// Task ExecuteActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) => Task.FromResult(false); + + + Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleExecutionActionOptions options); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index aecbf2eed..79b6525be 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -9,4 +9,6 @@ public class RuleActionContext public IEnumerable PrevStepResults { get; set; } = []; public IEnumerable NextActions { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } + public RuleNode Node { get; set; } + public RuleGraph Graph { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 75a13d398..ad419b603 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Templating; +using Microsoft.Extensions.Options; using System.Data; using System.Text.Json; @@ -52,67 +53,26 @@ public async Task> Triggered(IRuleTrigger trigger, string te } } - // Execute action - var ruleActions = rule.RuleActions?.Where(x => x != null && !string.IsNullOrEmpty(x.Name) && !x.Disabled) ?? []; - if (ruleActions.IsNullOrEmpty()) + // Execute actions + // 1. Load graph (agent id, rule name) + var graph = LoadGraph(); + + // 2. Get root node + var root = graph.GetRootNode(); + if (root == null) { continue; } - var actionIdx = 0; - var stepResults = new List(); - while (actionIdx >= 0 && actionIdx < ruleActions.Count()) - { - var ruleAction = ruleActions.ElementAt(actionIdx); - var dict = BuildContextParameters(ruleAction.Config, states, stepResults); - - var skipSteps = RenderSkippingExpression(ruleAction.SkippingExpression, dict); - if (skipSteps.HasValue && skipSteps > 0) - { - actionIdx += skipSteps.Value; - continue; - } - - var actionResult = await ExecuteActionAsync(agent, ruleAction, ruleActions.Skip(actionIdx + 1), trigger, text, dict, stepResults, options); - if (actionResult == null) - { - actionIdx++; - continue; - } - - if (!actionResult.Success) - { - break; - } - - stepResults.Add(new() - { - RuleAction = ruleAction, - Success = actionResult.Success, - Response = actionResult.Response, - ErrorMessage = actionResult.ErrorMessage, - Data = actionResult.Data - }); - - if (actionResult?.Success == true - && actionResult.Data.TryGetValue("conversation_id", out var convId) - && convId != null) - { - newConversationIds.Add(convId.ToString()!); - } - - if (actionResult?.IsDelayed == true) - { - break; - } - - actionIdx++; - } + // 3. Execute graph + var execResults = new List(); + await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); } return newConversationIds; } + [Obsolete] public async Task ExecuteActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) { var agentService = _services.GetRequiredService(); @@ -164,6 +124,166 @@ public async Task ExecuteActions(IRuleTrigger trigger, IEnumerable(); + var agent = await agentService.GetAgent(options.AgentId); + + var execResults = new List(); + await ExecuteGraphNode(node, graph, agent, trigger, options.Text, options.States, null, execResults); + } + + private RuleGraph LoadGraph() + { + var graph = RuleGraph.Init(); + var root = new RuleNode + { + Name = "root", + Type = "root", + }; + + var delayNode = new RuleNode + { + Name = "delay_message", + Type = "action", + Config = new() + { + ["delay"] = "3 seconds" + } + }; + + var node1 = new RuleNode + { + Name = "http_request", + Type = "action", + Config = new() + { + ["http_method"] = "GET", + ["http_url"] = "https://dummy.restapiexample.com/api/v1/employees" + } + }; + + var node2 = new RuleNode + { + Name = "http_request", + Type = "action", + Config = new() + { + ["http_method"] = "GET", + ["http_url"] = "https://dummy.restapiexample.com/api/v1/employee/1" + } + }; + + var node3 = new RuleNode + { + Name = "http_request", + Type = "action", + Config = new() + { + ["http_method"] = "GET", + ["http_url"] = "https://dummy.restapiexample.com/api/v1/employee/2" + } + }; + + graph.AddEdge(root, delayNode, payload: new() + { + Name = "edge", + Type = "is_next" + }); + + graph.AddEdge(delayNode, node1, payload: new() + { + Name = "edge", + Type = "is_next" + }); + + graph.AddEdge(node1, node2, payload: new() + { + Name = "edge", + Type = "is_next" + }); + + graph.AddEdge(node1, node3, payload: new() + { + Name = "edge", + Type = "is_next" + }); + + return graph; + } + + + private async Task ExecuteGraphNode( + RuleNode node, + RuleGraph graph, + Agent agent, + IRuleTrigger trigger, + string text, + IEnumerable? states, + RuleTriggerOptions? options, + List results) + { + var neighbors = graph.GetNeighbors(node); + foreach (var (neighborNode, edge) in neighbors) + { + if (!neighborNode.Type.IsEqualTo("action")) + { + continue; + } + + var actions = _services.GetServices(); + var action = actions.FirstOrDefault(x => x.Name.IsEqualTo(neighborNode.Name)); + if (action == null) + { + continue; + } + + var context = new RuleActionContext + { + Node = neighborNode, + Graph = graph, + Text = text, + Parameters = BuildContextParameters(neighborNode.Config, states), + PrevStepResults = results.Select(x => new RuleActionStepResult + { + Success = x.Success, + Response = x.Response, + ErrorMessage = x.ErrorMessage, + Data = x.Data + }), + JsonOptions = options?.JsonOptions + }; + + // Check whether the edge is executable from source node to target node + var isExecutable = IsExecutable(edge, agent, trigger, context); + if (!isExecutable) + { + continue; + } + + var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); + results.Add(actionResult); + + if (actionResult.IsDelayed) + { + continue; + } + + await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + } + } + + + private bool IsExecutable(RuleEdge edge, Agent agent, IRuleTrigger triger, RuleActionContext context) + { + return true; + } + #region Criteria private async Task ExecuteCriteriaAsync( @@ -224,6 +344,57 @@ private async Task ExecuteCriteriaAsync( #region Action + private async Task ExecuteAction( + RuleNode node, + RuleGraph graph, + Agent agent, + IRuleTrigger trigger, + RuleActionContext context) + { + try + { + // Get all registered rule actions + var actions = _services.GetServices(); + + // Find the matching action + var foundAction = actions.FirstOrDefault(x => x.Name.IsEqualTo(node?.Name)); + + if (foundAction == null) + { + var errorMsg = $"No rule action {node?.Name} is found"; + _logger.LogWarning(errorMsg); + return RuleActionResult.Failed(errorMsg); + } + + _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", + foundAction.Name, agent.Id, trigger.Name); + + var hooks = _services.GetHooks(agent.Id); + foreach (var hook in hooks) + { + await hook.BeforeRuleActionExecuted(agent, node, trigger, context); + } + + // Execute action + context.Parameters ??= []; + var result = await foundAction.ExecuteAsync(agent, trigger, context); + + foreach (var hook in hooks) + { + await hook.AfterRuleActionExecuted(agent, node, trigger, result); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", node?.Name, agent.Id); + return RuleActionResult.Failed(ex.Message); + } + } + + + [Obsolete] private async Task ExecuteActionAsync( Agent agent, AgentRuleAction curRuleAction, @@ -321,6 +492,39 @@ private async Task ExecuteActionAsync( return dict; } + private Dictionary BuildContextParameters(Dictionary? config, IEnumerable? states, IEnumerable? stepResults = null) + { + var dict = new Dictionary(); + + if (config != null) + { + dict = new(config); + } + + if (!states.IsNullOrEmpty()) + { + foreach (var state in states!) + { + dict[state.Key] = state.Value?.ConvertToString(); + } + } + + if (!stepResults.IsNullOrEmpty()) + { + foreach (var result in stepResults!) + { + if (result.Data.IsNullOrEmpty()) continue; + + foreach (var item in result.Data) + { + dict[item.Key] = item.Value; + } + } + } + + return dict; + } + private static Dictionary ConvertToDictionary(JsonDocument doc) { var dict = new Dictionary(); From 7e10997851695baec1d6aa53a0fa6d4c5d9f9bbf Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 27 Feb 2026 15:51:00 -0600 Subject: [PATCH 52/91] temp add deme rule trigger --- .../agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/agent.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/agent.json index e958bcb31..f09d98554 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/agent.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01e2fc5c-2c89-4ec7-8470-7688608b496c/agent.json @@ -14,5 +14,10 @@ "model": "gpt-5-mini", "max_recursion_depth": 3, "reasoning_effort_level": "minimal" - } + }, + "rules": [ + { + "trigger_name": "DemoRuleTrigger" + } + ] } \ No newline at end of file From fd73f2efc3be3273555a619536b62f8d45b25046 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 27 Feb 2026 16:39:41 -0600 Subject: [PATCH 53/91] clean code --- .../Agents/Models/AgentRule.cs | 24 ---- .../Rules/Hooks/IRuleTriggerHook.cs | 3 - .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 9 +- .../Rules/Models/RuleActionContext.cs | 1 - .../Rules/Models/RuleActionResult.cs | 1 - .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 116 ------------------ .../Models/AgentRuleMongoElement.cs | 45 +------ 7 files changed, 5 insertions(+), 194 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 2653feec2..9cef3490b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -13,10 +13,6 @@ public class AgentRule [JsonPropertyName("rule_criteria")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentRuleCriteria? RuleCriteria { get; set; } - - [JsonPropertyName("rule_actions")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IEnumerable RuleActions { get; set; } = []; } public class AgentRuleCriteria : AgentRuleConfigBase @@ -38,26 +34,6 @@ public class AgentRuleCriteria : AgentRuleConfigBase public override JsonDocument? Config { get; set; } } -public class AgentRuleAction : AgentRuleConfigBase -{ - /// - /// Adaptive configuration for rule actions. - /// This flexible JSON document can store any action-specific configuration. - /// The structure depends on the action type: - /// - For "Http" action: contains http_context with base_url, relative_url, method, etc. - /// - For "MessageQueue" action: contains mq_config with topic_name, routing_key, etc. - /// - For custom actions: can contain any custom configuration structure - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public override JsonDocument? Config { get; set; } - - /// - /// Skipping the number of actions using liquid template, starting from the action itself. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SkippingExpression { get; set; } -} - public class AgentRuleConfigBase { public virtual string Name { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index e70248cb3..52ccf048c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -8,9 +8,6 @@ public interface IRuleTriggerHook : IHookBase Task BeforeRuleCriteriaExecuted(Agent agent, AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; Task AfterRuleCriteriaExecuted(Agent agent, AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; - Task BeforeRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; - Task AfterRuleActionExecuted(Agent agent, AgentRuleAction ruleAction, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; - Task BeforeRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index 949b6dac9..09ebf09d9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -17,15 +17,12 @@ Task> Triggered(IRuleTrigger trigger, string text, IEnumerab => throw new NotImplementedException(); /// - /// Execute rule actions + /// Execute rule graph node /// + /// + /// /// - /// /// /// - Task ExecuteActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) - => Task.FromResult(false); - - Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleExecutionActionOptions options); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs index 79b6525be..20b5334fc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs @@ -7,7 +7,6 @@ public class RuleActionContext public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; public IEnumerable PrevStepResults { get; set; } = []; - public IEnumerable NextActions { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } public RuleNode Node { get; set; } public RuleGraph Graph { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs index 453d75a4c..aa119e5e9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs @@ -51,5 +51,4 @@ public static RuleActionResult Failed(string errorMessage) public class RuleActionStepResult : RuleActionResult { - public AgentRuleAction RuleAction { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index ad419b603..cdc0adf00 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Templating; -using Microsoft.Extensions.Options; using System.Data; using System.Text.Json; @@ -72,58 +71,6 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } - [Obsolete] - public async Task ExecuteActions(IRuleTrigger trigger, IEnumerable actions, RuleExecutionActionOptions options) - { - var agentService = _services.GetRequiredService(); - var agent = await agentService.GetAgent(options.AgentId); - - var actionIdx = 0; - var stepResults = new List(); - while (actionIdx >= 0 && actionIdx < actions.Count()) - { - var ruleAction = actions.ElementAt(actionIdx); - var dict = BuildContextParameters(ruleAction.Config, options.States, stepResults); - - var skipSteps = RenderSkippingExpression(ruleAction.SkippingExpression, dict); - if (skipSteps.HasValue && skipSteps > 0) - { - actionIdx += skipSteps.Value; - continue; - } - - var actionResult = await ExecuteActionAsync(agent, ruleAction, actions.Skip(actionIdx + 1), trigger, options.Text, dict, stepResults); - if (actionResult == null) - { - actionIdx++; - continue; - } - - if (!actionResult.Success) - { - break; - } - - stepResults.Add(new() - { - RuleAction = ruleAction, - Success = actionResult.Success, - Response = actionResult.Response, - ErrorMessage = actionResult.ErrorMessage, - Data = actionResult.Data - }); - - if (actionResult?.IsDelayed == true) - { - break; - } - - actionIdx++; - } - - return true; - } - public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleExecutionActionOptions options) { if (node == null || graph == null) @@ -392,69 +339,6 @@ private async Task ExecuteAction( return RuleActionResult.Failed(ex.Message); } } - - - [Obsolete] - private async Task ExecuteActionAsync( - Agent agent, - AgentRuleAction curRuleAction, - IEnumerable nextRuleActions, - IRuleTrigger trigger, - string text, - Dictionary param, - IEnumerable prevStepResults, - RuleTriggerOptions? triggerOptions = null) - { - try - { - // Get all registered rule actions - var actions = _services.GetServices(); - - // Find the matching action - var foundAction = actions.FirstOrDefault(x => x.Name.IsEqualTo(curRuleAction.Name)); - - if (foundAction == null) - { - var errorMsg = $"No rule action {curRuleAction.Name} is found"; - _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); - } - - var context = new RuleActionContext - { - Text = text, - Parameters = param, - PrevStepResults = prevStepResults, - NextActions = nextRuleActions, - JsonOptions = triggerOptions?.JsonOptions - }; - - _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", - foundAction.Name, agent.Id, trigger.Name); - - var hooks = _services.GetHooks(agent.Id); - foreach (var hook in hooks) - { - await hook.BeforeRuleActionExecuted(agent, curRuleAction, trigger, context); - } - - // Execute action - context.Parameters ??= []; - var result = await foundAction.ExecuteAsync(agent, trigger, context); - - foreach (var hook in hooks) - { - await hook.AfterRuleActionExecuted(agent, curRuleAction, trigger, result); - } - - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", curRuleAction.Name, agent.Id); - return RuleActionResult.Failed(ex.Message); - } - } #endregion diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 1bd76635a..d1a3e538a 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -9,7 +9,6 @@ public class AgentRuleMongoElement public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } - public List RuleActions { get; set; } = []; public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -17,8 +16,7 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria), - RuleActions = rule.RuleActions?.Where(x => x != null).Select(x => AgentRuleActionMongoElement.ToMongoElement(x)!)?.ToList() ?? [] + RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria) }; } @@ -28,8 +26,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria), - RuleActions = rule.RuleActions?.Where(x => x != null).Select(x => AgentRuleActionMongoElement.ToDomainElement(x)!)?.ToList() ?? [] + RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria) }; } } @@ -72,44 +69,6 @@ public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel } } -[BsonIgnoreExtraElements(Inherited = true)] -public class AgentRuleActionMongoElement : AgentRuleConfigMongoModel -{ - public string? SkippingExpression { get; set; } - - public static AgentRuleActionMongoElement? ToMongoElement(AgentRuleAction? action) - { - if (action == null) - { - return null; - } - - return new AgentRuleActionMongoElement - { - Name = action.Name, - Disabled = action.Disabled, - Config = action.Config != null ? BsonDocument.Parse(action.Config.RootElement.GetRawText()) : null, - SkippingExpression = action.SkippingExpression - }; - } - - public static AgentRuleAction? ToDomainElement(AgentRuleActionMongoElement? action) - { - if (action == null) - { - return null; - } - - return new AgentRuleAction - { - Name = action.Name, - Disabled = action.Disabled, - Config = action.Config != null ? JsonDocument.Parse(action.Config.ToJson()) : null, - SkippingExpression = action.SkippingExpression - }; - } -} - [BsonIgnoreExtraElements(Inherited = true)] public class AgentRuleConfigMongoModel { From be64936b49c3a20d0b037e7b2b0256825b2729db Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Fri, 27 Feb 2026 22:20:35 -0600 Subject: [PATCH 54/91] use step result --- .../Rules/Models/RuleActionResult.cs | 1 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 36 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs index aa119e5e9..ea0c33a6e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs @@ -51,4 +51,5 @@ public static RuleActionResult Failed(string errorMessage) public class RuleActionStepResult : RuleActionResult { + public RuleNode Node { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index cdc0adf00..c87f75603 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -55,6 +55,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute actions // 1. Load graph (agent id, rule name) var graph = LoadGraph(); + if (graph == null) + { + continue; + } // 2. Get root node var root = graph.GetRootNode(); @@ -64,8 +68,15 @@ public async Task> Triggered(IRuleTrigger trigger, string te } // 3. Execute graph - var execResults = new List(); + var execResults = new List(); await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); + + var convIds = execResults.Where(x => x.Success && x.Data.TryGetValue("conversation_id", out _)) + .Select(x => x.Data.GetValueOrDefault("conversation_id", string.Empty)) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + newConversationIds.AddRange(convIds); } return newConversationIds; @@ -81,7 +92,7 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger var agentService = _services.GetRequiredService(); var agent = await agentService.GetAgent(options.AgentId); - var execResults = new List(); + var execResults = new List(); await ExecuteGraphNode(node, graph, agent, trigger, options.Text, options.States, null, execResults); } @@ -173,7 +184,7 @@ private async Task ExecuteGraphNode( string text, IEnumerable? states, RuleTriggerOptions? options, - List results) + List results) { var neighbors = graph.GetNeighbors(node); foreach (var (neighborNode, edge) in neighbors) @@ -196,13 +207,7 @@ private async Task ExecuteGraphNode( Graph = graph, Text = text, Parameters = BuildContextParameters(neighborNode.Config, states), - PrevStepResults = results.Select(x => new RuleActionStepResult - { - Success = x.Success, - Response = x.Response, - ErrorMessage = x.ErrorMessage, - Data = x.Data - }), + PrevStepResults = results, JsonOptions = options?.JsonOptions }; @@ -213,8 +218,17 @@ private async Task ExecuteGraphNode( continue; } + // Execute action var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); - results.Add(actionResult); + results.Add(new RuleActionStepResult + { + Node = neighborNode, + Success = actionResult.Success, + Response = actionResult.Response, + Data = new(actionResult.Data ?? []), + ErrorMessage = actionResult.ErrorMessage, + IsDelayed = actionResult.IsDelayed + }); if (actionResult.IsDelayed) { From cf9cbf5d55fd7d6189eb3f193410a52d525273dc Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 2 Mar 2026 12:13:34 -0600 Subject: [PATCH 55/91] refine graph attributes --- .../Agents/Models/RuleGraph.cs | 13 ++++++- .../Options/RuleExecutionActionOptions.cs | 1 + .../Rules/Options/RuleTriggerOptions.cs | 5 +++ .../Constants/RuleConstant.cs | 1 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 38 ++++++++++++++++--- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index b4c750ed1..52753d27f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -2,11 +2,13 @@ namespace BotSharp.Abstraction.Agents.Models; public class RuleGraph { + private string _id = Guid.NewGuid().ToString(); private List _nodes = []; private List _edges = []; public RuleGraph() { + _id = Guid.NewGuid().ToString(); _nodes = []; _edges = []; } @@ -36,6 +38,11 @@ public IEnumerable GetEdges() return [.. _edges]; } + public void SetGraphId(string id) + { + _id = id; + } + public void SetNodes(IEnumerable nodes) { _nodes = [.. nodes?.ToList() ?? []]; @@ -86,13 +93,15 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) public IEnumerable<(RuleNode, RuleEdge)> GetNeighbors(RuleNode node) { return _edges.Where(e => e.From != null && e.From.Id.IsEqualTo(node.Id)) - .Select(e => (e.To, e)); + .Select(e => (e.To, e)) + .ToList(); } public RuleGraphInfo GetGraphInfo() { return new() { + GraphId = _id, Nodes = _nodes, Edges = _edges }; @@ -101,6 +110,7 @@ public RuleGraphInfo GetGraphInfo() public static RuleGraph FromGraphInfo(RuleGraphInfo graphInfo) { var graph = new RuleGraph(); + graph.SetGraphId(graphInfo.GraphId.IfNullOrEmptyAs(Guid.NewGuid().ToString())!); graph.SetNodes(graphInfo.Nodes); graph.SetEdges(graphInfo.Edges); return graph; @@ -162,6 +172,7 @@ public GraphItemPayload() public class RuleGraphInfo { + public string GraphId { get; set; } public IEnumerable Nodes { get; set; } = []; public IEnumerable Edges { get; set; } = []; } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs index 48787e06e..196104084 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs @@ -5,4 +5,5 @@ public class RuleExecutionActionOptions public string AgentId { get; set; } public string Text { get; set; } public IEnumerable States { get; set; } = []; + public int? MaxGraphRecursion { get; set; } = 10; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index abba98115..77def58c0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -14,4 +14,9 @@ public class RuleTriggerOptions /// Json serializer options /// public JsonSerializerOptions? JsonOptions { get; set; } + + /// + /// Max number of action node execution (prevent endless loop) + /// + public int? MaxGraphRecursion { get; set; } = 10; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index daf54db77..007b5013f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -3,4 +3,5 @@ namespace BotSharp.Core.Rules.Constants; public static class RuleConstant { public const string DEFAULT_CRITERIA_PROVIDER = "code_script"; + public const int MAX_GRAPH_RECURSION = 10; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index c87f75603..19aaa3ccd 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -54,7 +54,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute actions // 1. Load graph (agent id, rule name) - var graph = LoadGraph(); + var graph = await LoadGraph(); if (graph == null) { continue; @@ -84,7 +84,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleExecutionActionOptions options) { - if (node == null || graph == null) + if (node == null || graph == null || options == null) { return; } @@ -92,11 +92,22 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger var agentService = _services.GetRequiredService(); var agent = await agentService.GetAgent(options.AgentId); + var triggerOptions = new RuleTriggerOptions + { + MaxGraphRecursion = options.MaxGraphRecursion + }; + var execResults = new List(); - await ExecuteGraphNode(node, graph, agent, trigger, options.Text, options.States, null, execResults); + await ExecuteGraphNode( + node, graph, + agent, trigger, + options.Text, + options.States, + triggerOptions, + execResults); } - private RuleGraph LoadGraph() + private async Task LoadGraph() { var graph = RuleGraph.Init(); var root = new RuleNode @@ -186,9 +197,24 @@ private async Task ExecuteGraphNode( RuleTriggerOptions? options, List results) { + var maxRecursion = options?.MaxGraphRecursion ?? RuleConstant.MAX_GRAPH_RECURSION; + if (results.Count >= maxRecursion) + { + _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", + maxRecursion, agent.Name, trigger.Name); + return; + } + var neighbors = graph.GetNeighbors(node); foreach (var (neighborNode, edge) in neighbors) { + if (results.Count >= maxRecursion) + { + _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", + maxRecursion, agent.Name, trigger.Name); + break; + } + if (!neighborNode.Type.IsEqualTo("action")) { continue; @@ -212,7 +238,7 @@ private async Task ExecuteGraphNode( }; // Check whether the edge is executable from source node to target node - var isExecutable = IsExecutable(edge, agent, trigger, context); + var isExecutable = await IsExecutable(edge, agent, trigger, context, results); if (!isExecutable) { continue; @@ -240,7 +266,7 @@ private async Task ExecuteGraphNode( } - private bool IsExecutable(RuleEdge edge, Agent agent, IRuleTrigger triger, RuleActionContext context) + private async Task IsExecutable(RuleEdge edge, Agent agent, IRuleTrigger triger, RuleActionContext context, IEnumerable prevStepResults) { return true; } From c054542db322efef3ed890cb5e3ac99a8b3b46ec Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 2 Mar 2026 15:54:39 -0600 Subject: [PATCH 56/91] minor change --- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 19aaa3ccd..62676610d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -238,7 +238,7 @@ private async Task ExecuteGraphNode( }; // Check whether the edge is executable from source node to target node - var isExecutable = await IsExecutable(edge, agent, trigger, context, results); + var isExecutable = await IsExecutable(edge, agent, trigger, context); if (!isExecutable) { continue; @@ -266,7 +266,7 @@ private async Task ExecuteGraphNode( } - private async Task IsExecutable(RuleEdge edge, Agent agent, IRuleTrigger triger, RuleActionContext context, IEnumerable prevStepResults) + private async Task IsExecutable(RuleEdge edge, Agent agent, IRuleTrigger triger, RuleActionContext context) { return true; } From 5e304b011014bc584b5635a485099970d0350068 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 2 Mar 2026 15:57:06 -0600 Subject: [PATCH 57/91] remove step results from params --- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 62676610d..5de7b6591 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -383,7 +383,7 @@ private async Task ExecuteAction( #region Private methods - private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states, IEnumerable? stepResults = null) + private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states) { var dict = new Dictionary(); @@ -400,23 +400,10 @@ private async Task ExecuteAction( } } - if (!stepResults.IsNullOrEmpty()) - { - foreach (var result in stepResults!) - { - if (result.Data.IsNullOrEmpty()) continue; - - foreach (var item in result.Data) - { - dict[item.Key] = item.Value; - } - } - } - return dict; } - private Dictionary BuildContextParameters(Dictionary? config, IEnumerable? states, IEnumerable? stepResults = null) + private Dictionary BuildContextParameters(Dictionary? config, IEnumerable? states) { var dict = new Dictionary(); @@ -433,19 +420,6 @@ private async Task ExecuteAction( } } - if (!stepResults.IsNullOrEmpty()) - { - foreach (var result in stepResults!) - { - if (result.Data.IsNullOrEmpty()) continue; - - foreach (var item in result.Data) - { - dict[item.Key] = item.Value; - } - } - } - return dict; } From adeb942d2be7401adfdc41275e324edf785edd91 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 2 Mar 2026 16:10:12 -0600 Subject: [PATCH 58/91] refine rule graph --- .../BotSharp.Abstraction/Agents/Models/RuleGraph.cs | 10 ++++++++++ .../Rules/Options/RuleExecutionActionOptions.cs | 2 +- .../Rules/Options/RuleTriggerOptions.cs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 52753d27f..9219b0f4a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -28,6 +28,11 @@ public static RuleGraph Init() return _nodes.FirstOrDefault(x => x.Id.IsEqualTo(id)); } + public string GetGraphId() + { + return _id; + } + public IEnumerable GetNodes() { return [.. _nodes]; @@ -115,6 +120,11 @@ public static RuleGraph FromGraphInfo(RuleGraphInfo graphInfo) graph.SetEdges(graphInfo.Edges); return graph; } + + public override string ToString() + { + return $"Graph ({_id}) => Nodes: {_nodes.Count}, Edges: {_edges.Count}"; + } } public class RuleNode : GraphItemPayload diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs index 196104084..cd14b7aa0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs @@ -5,5 +5,5 @@ public class RuleExecutionActionOptions public string AgentId { get; set; } public string Text { get; set; } public IEnumerable States { get; set; } = []; - public int? MaxGraphRecursion { get; set; } = 10; + public int? MaxGraphRecursion { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 77def58c0..90beff810 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -18,5 +18,5 @@ public class RuleTriggerOptions /// /// Max number of action node execution (prevent endless loop) /// - public int? MaxGraphRecursion { get; set; } = 10; + public int? MaxGraphRecursion { get; set; } } From 90d86dfd02cd028077216be5a9955ec0a285a9dd Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 3 Mar 2026 12:04:07 -0600 Subject: [PATCH 59/91] rename --- .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 2 +- .../Rules/Options/RuleGraphOptions.cs | 5 +++ ...Options.cs => RuleNodeExecutionOptions.cs} | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 45 ++++++++++++++----- 4 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs rename src/Infrastructure/BotSharp.Abstraction/Rules/Options/{RuleExecutionActionOptions.cs => RuleNodeExecutionOptions.cs} (85%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index 09ebf09d9..8811bc358 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -24,5 +24,5 @@ Task> Triggered(IRuleTrigger trigger, string text, IEnumerab /// /// /// - Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleExecutionActionOptions options); + Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleNodeExecutionOptions options); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs new file mode 100644 index 000000000..945a06c97 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs @@ -0,0 +1,5 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleGraphOptions +{ +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs similarity index 85% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs index cd14b7aa0..a6b4e9099 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleExecutionActionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Rules.Options; -public class RuleExecutionActionOptions +public class RuleNodeExecutionOptions { public string AgentId { get; set; } public string Text { get; set; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 5de7b6591..8b09f63b7 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -54,7 +54,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // Execute actions // 1. Load graph (agent id, rule name) - var graph = await LoadGraph(); + var graph = await LoadGraph(agent.Id, trigger); if (graph == null) { continue; @@ -82,7 +82,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } - public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleExecutionActionOptions options) + public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleNodeExecutionOptions options) { if (node == null || graph == null || options == null) { @@ -107,7 +107,7 @@ await ExecuteGraphNode( execResults); } - private async Task LoadGraph() + private async Task LoadGraph(string agentId, IRuleTrigger trigger, RuleGraphOptions? options = null) { var graph = RuleGraph.Init(); var root = new RuleNode @@ -187,6 +187,31 @@ private async Task LoadGraph() } + private async Task LoadDefaultGraph() + { + var graph = RuleGraph.Init(); + var root = new RuleNode + { + Name = "root", + Type = "root", + }; + + var node = new RuleNode + { + Name = "send_message_to_agent", + Type = "action" + }; + + graph.AddEdge(root, node, payload: new() + { + Name = "edge", + Type = "is_next" + }); + + return graph; + } + + private async Task ExecuteGraphNode( RuleNode node, RuleGraph graph, @@ -208,13 +233,6 @@ private async Task ExecuteGraphNode( var neighbors = graph.GetNeighbors(node); foreach (var (neighborNode, edge) in neighbors) { - if (results.Count >= maxRecursion) - { - _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", - maxRecursion, agent.Name, trigger.Name); - break; - } - if (!neighborNode.Type.IsEqualTo("action")) { continue; @@ -256,6 +274,13 @@ private async Task ExecuteGraphNode( IsDelayed = actionResult.IsDelayed }); + if (results.Count >= maxRecursion) + { + _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", + maxRecursion, agent.Name, trigger.Name); + break; + } + if (actionResult.IsDelayed) { continue; From 871c617eff5e2cbc652520ec82ac358e7dc0e262 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 3 Mar 2026 14:37:26 -0600 Subject: [PATCH 60/91] refine to string --- .../BotSharp.Abstraction/Agents/Models/RuleGraph.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 9219b0f4a..7e117c522 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -136,7 +136,7 @@ public class RuleNode : GraphItemPayload public override string ToString() { - return $"Node: {Name} ({Type})"; + return $"Node ({Id}): {Name} ({Type})"; } } @@ -163,7 +163,7 @@ public RuleEdge(RuleNode from, RuleNode to) public override string ToString() { - return $"Edge: {Name} ({Type}), Connects from Node ({From?.Name}) to Node ({To?.Name})"; + return $"Edge ({Id}): {Name} ({Type}), Connects from Node ({From?.Name}) to Node ({To?.Name})"; } } From 5a5d739771eb3b1cc4a59fce13121c1f33afa735 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 4 Mar 2026 14:54:10 -0600 Subject: [PATCH 61/91] add rule condition and clean code --- .../Agents/Models/AgentRule.cs | 38 -- .../Agents/Models/RuleGraph.cs | 5 +- .../Rules/Hooks/IRuleTriggerHook.cs | 8 +- .../BotSharp.Abstraction/Rules/IRuleAction.cs | 19 +- .../Rules/IRuleCondition.cs | 22 + .../BotSharp.Abstraction/Rules/IRuleConfig.cs | 5 - .../Rules/IRuleCriteria.cs | 14 - .../Rules/IRuleFlowUnit.cs | 21 + .../Rules/Models/RuleActionResult.cs | 55 --- .../Rules/Models/RuleCriteriaContext.cs | 10 - .../Rules/Models/RuleCriteriaResult.cs | 19 - ...uleActionContext.cs => RuleFlowContext.cs} | 8 +- .../Rules/Models/RuleFlowStepResult.cs | 23 ++ .../Rules/Models/RuleNodeResult.cs | 34 ++ .../Actions/ChatRuleAction.cs | 12 +- .../Actions/FunctionCallRuleAction.cs | 12 +- .../Actions/HttpRuleAction.cs | 43 +- .../Conditions/ExampleRuleCondition.cs | 71 ++++ .../Constants/RuleConstant.cs | 18 + .../Criteria/CodeScriptRuleCriteria.cs | 140 ------- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 378 +++++++++--------- .../BotSharp.Core.Rules/RulesPlugin.cs | 7 +- .../Controllers/Agent/AgentController.Rule.cs | 20 - .../Models/AgentRuleMongoElement.cs | 54 +-- 24 files changed, 444 insertions(+), 592 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCondition.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs rename src/Infrastructure/BotSharp.Abstraction/Rules/Models/{RuleActionContext.cs => RuleFlowContext.cs} (62%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 9cef3490b..8f9b6b2db 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -1,5 +1,3 @@ -using System.Text.Json; - namespace BotSharp.Abstraction.Agents.Models; public class AgentRule @@ -9,40 +7,4 @@ public class AgentRule [JsonPropertyName("disabled")] public bool Disabled { get; set; } - - [JsonPropertyName("rule_criteria")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public AgentRuleCriteria? RuleCriteria { get; set; } -} - -public class AgentRuleCriteria : AgentRuleConfigBase -{ - /// - /// Criteria - /// - [JsonPropertyName("criteria_text")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string CriteriaText { get; set; } = string.Empty; - - /// - /// Adaptive configuration for rule criteria. - /// This flexible JSON document can store any criteria-specific configuration. - /// The structure depends on the criteria executor - /// - [JsonPropertyName("config")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public override JsonDocument? Config { get; set; } } - -public class AgentRuleConfigBase -{ - public virtual string Name { get; set; } - - public virtual bool Disabled { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public virtual JsonDocument? Config { get; set; } - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public virtual string? JsonConfig { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 7e117c522..7a1f32bd1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -20,7 +20,7 @@ public static RuleGraph Init() public RuleNode? GetRootNode() { - return _nodes.FirstOrDefault(x => x.Type.IsEqualTo("root")); + return _nodes.FirstOrDefault(x => x.Type.IsEqualTo("root") || x.Type.IsEqualTo("start")); } public RuleNode? GetNode(string id) @@ -171,7 +171,8 @@ public class GraphItemPayload { public virtual string Id { get; set; } = Guid.NewGuid().ToString(); public virtual string Name { get; set; } = null!; - public virtual string Type { get; set; } = "is_next"; + public virtual string Type { get; set; } = null!; + public virtual IEnumerable Labels { get; set; } = []; public virtual Dictionary Config { get; set; } = []; public GraphItemPayload() diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 52ccf048c..101a191fc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -5,9 +5,9 @@ namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { - Task BeforeRuleCriteriaExecuted(Agent agent, AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, RuleCriteriaContext context) => Task.CompletedTask; - Task AfterRuleCriteriaExecuted(Agent agent, AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, RuleCriteriaResult result) => Task.CompletedTask; + Task BeforeRuleConditionExecuted(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; + Task AfterRuleConditionExecuted(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleNodeResult result) => Task.CompletedTask; - Task BeforeRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleActionContext context) => Task.CompletedTask; - Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleActionResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleNodeResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs index ee2d43e68..041675a2c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleAction.cs @@ -1,32 +1,21 @@ using BotSharp.Abstraction.Rules.Models; -using System.Text.Json; namespace BotSharp.Abstraction.Rules; /// /// Base interface for rule actions that can be executed by the RuleEngine /// -public interface IRuleAction +public interface IRuleAction : IRuleFlowUnit { - /// - /// The unique name of the rule action provider - /// - string Name { get; } - - /// - /// The default config json format. - /// - JsonDocument DefaultConfig => JsonDocument.Parse("{}"); - /// /// Execute the rule action /// /// The agent that triggered the rule /// The rule trigger - /// The action context + /// The flow context /// The action execution result - Task ExecuteAsync( + Task ExecuteAsync( Agent agent, IRuleTrigger trigger, - RuleActionContext context); + RuleFlowContext context); } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCondition.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCondition.cs new file mode 100644 index 000000000..745ad3349 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCondition.cs @@ -0,0 +1,22 @@ +using BotSharp.Abstraction.Rules.Models; + +namespace BotSharp.Abstraction.Rules; + +/// +/// Base interface for rule conditions that can be evaluated by the RuleEngine +/// +public interface IRuleCondition : IRuleFlowUnit +{ + /// + /// Evaluate the rule condition + /// + /// The agent that triggered the rule + /// The rule trigger + /// The flow context + /// The condition evaluation result + Task EvaluateAsync( + Agent agent, + IRuleTrigger trigger, + RuleFlowContext context); +} + diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs deleted file mode 100644 index dcbe18271..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace BotSharp.Abstraction.Rules; - -public interface IRuleConfig -{ -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs deleted file mode 100644 index 30658e582..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteria.cs +++ /dev/null @@ -1,14 +0,0 @@ -using BotSharp.Abstraction.Rules.Models; -using System.Text.Json; - -namespace BotSharp.Abstraction.Rules; - -public interface IRuleCriteria -{ - string Provider { get; } - - JsonDocument DefaultConfig => JsonDocument.Parse("{}"); - - Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) - => Task.FromResult(new RuleCriteriaResult()); -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs new file mode 100644 index 000000000..795b2ce01 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs @@ -0,0 +1,21 @@ +using BotSharp.Abstraction.Rules.Models; + +namespace BotSharp.Abstraction.Rules; + +public interface IRuleFlowUnit +{ + /// + /// The unique name of the rule flow unit, i.e., action, condition. + /// + string Name => string.Empty; + + /// + /// The agent id + /// + string? AgentId => null; + + /// + /// The trigger names + /// + IEnumerable? Triggers => null; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs deleted file mode 100644 index ea0c33a6e..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionResult.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Models; - -/// -/// Result of a rule action execution -/// -public class RuleActionResult -{ - /// - /// Whether the action executed successfully - /// - public bool Success { get; set; } - - /// - /// Response content from the action - /// - public string? Response { get; set; } - - /// - /// Result data - /// - public Dictionary Data { get; set; } = []; - - /// - /// Error message if the action failed - /// - public string? ErrorMessage { get; set; } - - /// - /// Whether the action is delayed - /// - public bool IsDelayed { 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 - }; - } -} - -public class RuleActionStepResult : RuleActionResult -{ - public RuleNode Node { get; set; } -} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs deleted file mode 100644 index 7fff33344..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json; - -namespace BotSharp.Abstraction.Rules.Models; - -public class RuleCriteriaContext -{ - public string Text { get; set; } = string.Empty; - public Dictionary Parameters { get; set; } = []; - public JsonSerializerOptions? JsonOptions { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs deleted file mode 100644 index fc1df6ceb..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Models; - -public class RuleCriteriaResult -{ - /// - /// Whether the criteria executed successfully - /// - public bool Success { get; set; } - - /// - /// Response content from the action - /// - public bool IsValid { get; set; } - - /// - /// Error message if the criteria failed - /// - public string? ErrorMessage { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs similarity index 62% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs index 20b5334fc..7fd0f7a18 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleActionContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs @@ -2,12 +2,16 @@ namespace BotSharp.Abstraction.Rules.Models; -public class RuleActionContext +/// +/// Context for rule flow execution (actions and conditions) +/// +public class RuleFlowContext { public string Text { get; set; } = string.Empty; public Dictionary Parameters { get; set; } = []; - public IEnumerable PrevStepResults { get; set; } = []; + public IEnumerable PrevStepResults { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } public RuleNode Node { get; set; } public RuleGraph Graph { get; set; } } + diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs new file mode 100644 index 000000000..5acf337b6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs @@ -0,0 +1,23 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleFlowStepResult : RuleNodeResult +{ + public RuleNode Node { get; set; } + + /// + /// Create a RuleFlowStepResult from a RuleNodeResult and a RuleNode + /// + public static RuleFlowStepResult FromResult(RuleNodeResult result, RuleNode node) + { + return new RuleFlowStepResult + { + Node = node, + Success = result.Success, + IsValid = result.IsValid, + Response = result.Response, + ErrorMessage = result.ErrorMessage, + Data = new(result.Data ?? []), + IsDelayed = result.IsDelayed + }; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs new file mode 100644 index 000000000..a9020f284 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs @@ -0,0 +1,34 @@ +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleNodeResult +{ + /// + /// Whether the node is executed successfully + /// + public virtual bool Success { get; set; } + + /// + /// Whether the node evaluation is valid (used for conditions) + /// + public virtual bool IsValid { get; set; } + + /// + /// Response content from the node + /// + public virtual string? Response { get; set; } + + /// + /// Error message if the node execution failed + /// + public virtual string? ErrorMessage { get; set; } + + /// + /// Result data (used for actions) + /// + public virtual Dictionary Data { get; set; } = []; + + /// + /// Whether the node execution is delayed (used for actions) + /// + public virtual bool IsDelayed { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index eae15d74e..257b874f0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -15,10 +15,10 @@ public ChatRuleAction( public string Name => "send_message_to_agent"; - public async Task ExecuteAsync( + public async Task ExecuteAsync( Agent agent, IRuleTrigger trigger, - RuleActionContext context) + RuleFlowContext context) { using var scope = _services.CreateScope(); var sp = scope.ServiceProvider; @@ -57,7 +57,7 @@ await convService.SendMessage(agent.Id, _logger.LogInformation("Chat rule action executed successfully for agent {AgentId}, conversation {ConversationId}", agent.Id, conv.Id); - return new RuleActionResult + return new RuleNodeResult { Success = true, Response = conv.Id, @@ -71,7 +71,11 @@ await convService.SendMessage(agent.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); + return new RuleNodeResult + { + Success = false, + ErrorMessage = ex.Message + }; } } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs index 7251a24d7..e9b040e76 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/FunctionCallRuleAction.cs @@ -17,10 +17,10 @@ public FunctionCallRuleAction( public string Name => "function_call"; - public async Task ExecuteAsync( + public async Task ExecuteAsync( Agent agent, IRuleTrigger trigger, - RuleActionContext context) + RuleFlowContext context) { var funcName = context.Parameters.TryGetValue("function_name", out var fName) ? fName : null; var func = _services.GetServices().FirstOrDefault(x => x.Name.IsEqualTo(funcName)); @@ -29,13 +29,17 @@ public async Task ExecuteAsync( { var errorMsg = $"Unable to find function '{funcName}' when running action {agent.Name}-{trigger.Name}"; _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); + return new RuleNodeResult + { + Success = false, + ErrorMessage = errorMsg + }; } var funcArg = context.Parameters.TryGetObjectValueOrDefault("function_argument", new()) ?? new(); await func.Execute(funcArg); - return new RuleActionResult + return new RuleNodeResult { Success = true, Response = funcArg?.RichContent?.Message?.Text ?? funcArg?.Content, diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index 18aa2446a..403faa333 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -1,5 +1,4 @@ using System.Net.Mime; -using System.Text.Json; using System.Web; namespace BotSharp.Core.Rules.Actions; @@ -22,16 +21,16 @@ public HttpRuleAction( public string Name => "http_request"; - public JsonDocument DefaultConfig => JsonDocument.Parse(JsonSerializer.Serialize(new - { - http_url = "https://dummy.example.com/api/v1/employees", - http_method = "GET" - })); + // Default configuration example: + // { + // "http_url": "https://dummy.example.com/api/v1/employees", + // "http_method": "GET" + // } - public async Task ExecuteAsync( + public async Task ExecuteAsync( Agent agent, IRuleTrigger trigger, - RuleActionContext context) + RuleFlowContext context) { try { @@ -40,7 +39,11 @@ public async Task ExecuteAsync( { var errorMsg = $"HTTP method is not supported in agent rule {agent.Name}-{trigger.Name}"; _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); + return new RuleNodeResult + { + Success = false, + ErrorMessage = errorMsg + }; } // Build the full URL @@ -73,7 +76,7 @@ public async Task ExecuteAsync( _logger.LogInformation("HTTP rule action executed successfully for agent {AgentId}, Status: {StatusCode}, Response: {Response}", agent.Id, response.StatusCode, responseContent); - return new RuleActionResult + return new RuleNodeResult { Success = true, Response = responseContent, @@ -87,18 +90,26 @@ public async Task ExecuteAsync( { var errorMsg = $"HTTP request failed with status code {response.StatusCode}: {responseContent}"; _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); + return new RuleNodeResult + { + Success = false, + ErrorMessage = errorMsg + }; } } catch (Exception ex) { _logger.LogError(ex, "Error executing HTTP rule action for agent {AgentId} and trigger {TriggerName}", agent.Id, trigger.Name); - return RuleActionResult.Failed(ex.Message); + return new RuleNodeResult + { + Success = false, + ErrorMessage = ex.Message + }; } } - private string BuildUrl(RuleActionContext context) + private string BuildUrl(RuleFlowContext context) { var url = context.Parameters.GetValueOrDefault("http_url", string.Empty); if (string.IsNullOrEmpty(url)) @@ -139,7 +150,7 @@ private string BuildUrl(RuleActionContext context) return url; } - private HttpMethod? GetHttpMethod(RuleActionContext context) + private HttpMethod? GetHttpMethod(RuleFlowContext context) { var method = context.Parameters.GetValueOrDefault("http_method", string.Empty); var innerMethod = method?.Trim()?.ToUpper(); @@ -170,7 +181,7 @@ private string BuildUrl(RuleActionContext context) return matchMethod; } - private void AddHttpHeaders(HttpClient client, RuleActionContext context) + private void AddHttpHeaders(HttpClient client, RuleFlowContext context) { var headerParams = context.Parameters.TryGetObjectValueOrDefault>("http_request_headers"); if (!headerParams.IsNullOrEmpty()) @@ -182,7 +193,7 @@ private void AddHttpHeaders(HttpClient client, RuleActionContext context) } } - private string? GetHttpRequestBody(RuleActionContext context) + private string? GetHttpRequestBody(RuleFlowContext context) { var body = context.Parameters.GetValueOrDefault("http_request_body"); return body; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs new file mode 100644 index 000000000..24af33062 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs @@ -0,0 +1,71 @@ +namespace BotSharp.Core.Rules.Conditions; + +/// +/// Example rule condition that demonstrates how to implement IRuleCondition. +/// This condition checks if a parameter value matches an expected value. +/// +public sealed class ExampleRuleCondition : IRuleCondition +{ + private readonly ILogger _logger; + + public ExampleRuleCondition(ILogger logger) + { + _logger = logger; + } + + public string Name => "example_condition"; + + // Default configuration example: + // { + // "parameter_name": "status", + // "expected_value": "active" + // } + + public async Task EvaluateAsync( + Agent agent, + IRuleTrigger trigger, + RuleFlowContext context) + { + try + { + var parameterName = context.Parameters.GetValueOrDefault("parameter_name", "status"); + var expectedValue = context.Parameters.GetValueOrDefault("expected_value", "active"); + var actualValue = context.Parameters.GetValueOrDefault(parameterName, string.Empty); + + _logger.LogInformation("Evaluating condition: {ParameterName} = {ActualValue}, expected = {ExpectedValue}", + parameterName, actualValue, expectedValue); + + var isMatch = actualValue?.Equals(expectedValue, StringComparison.OrdinalIgnoreCase) == true; + + if (isMatch) + { + return new RuleNodeResult + { + Success = true, + IsValid = true, + Response = $"Condition met: {parameterName} = {actualValue}" + }; + } + else + { + return new RuleNodeResult + { + Success = true, + IsValid = false, + Response = $"Condition not met: {parameterName} = {actualValue}, expected = {expectedValue}" + }; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error evaluating example condition for agent {AgentId}", agent.Id); + return new RuleNodeResult + { + Success = false, + IsValid = false, + ErrorMessage = ex.Message + }; + } + } +} + diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index 007b5013f..a7a773a96 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -4,4 +4,22 @@ public static class RuleConstant { public const string DEFAULT_CRITERIA_PROVIDER = "code_script"; public const int MAX_GRAPH_RECURSION = 10; + + public static IEnumerable CONDITION_NODE_TYPES = new List + { + "condition", + "criteria" + }; + + public static IEnumerable ACTION_NODE_TYPES = new List + { + "action" + }; + + public static IEnumerable END_NODE_TYPES = new List + { + "root", + "start", + "end" + }; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs deleted file mode 100644 index 31536844f..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Criteria/CodeScriptRuleCriteria.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Text.Json; - -namespace BotSharp.Core.Rules.Criteria; - -public class CodeScriptRuleCriteria : IRuleCriteria -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private readonly CodingSettings _codingSettings; - - public CodeScriptRuleCriteria( - IServiceProvider services, - ILogger logger, - CodingSettings codingSettings) - { - _services = services; - _logger = logger; - _codingSettings = codingSettings; - } - - public string Provider => RuleConstant.DEFAULT_CRITERIA_PROVIDER; - - public JsonDocument DefaultConfig => JsonDocument.Parse(JsonSerializer.Serialize(new - { - code_processor = BuiltInCodeProcessor.PyInterpreter, - code_script_name = "{trigger_name}_rule.py", - code_script_arg_name = "trigger_args", - code_script_arg_value = JsonDocument.Parse("{}") - })); - - public async Task ValidateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) - { - var result = new RuleCriteriaResult(); - - if (string.IsNullOrWhiteSpace(agent?.Id)) - { - return result; - } - - var provider = context.Parameters.GetValueOrDefault("code_processor", BuiltInCodeProcessor.PyInterpreter); - var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); - if (processor == null) - { - _logger.LogWarning($"Unable to find code processor: {provider}."); - return result; - } - - var agentService = _services.GetRequiredService(); - var scriptName = context.Parameters.GetValueOrDefault("code_script_name", $"{trigger.Name}_rule.py"); - var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); - - var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name})."; - - if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) - { - _logger.LogWarning($"Unable to find {msg}."); - return result; - } - - try - { - var hooks = _services.GetHooks(agent.Id); - - var argName = context.Parameters.GetValueOrDefault("code_script_arg_name", null); - var argValue = context.Parameters.TryGetValue("code_script_arg_value", out var val) && val != null ? JsonSerializer.Deserialize(val) : (JsonElement?)null; - var arguments = BuildArguments(argName, argValue); - var codeExecutionContext = new CodeExecutionContext - { - CodeScript = codeScript, - Arguments = arguments, - InvokeFrom = nameof(CodeScriptRuleCriteria) - }; - - foreach (var hook in hooks) - { - await hook.BeforeCodeExecution(agent, codeExecutionContext); - } - - codeScript = codeExecutionContext.CodeScript; - 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 = codeExecutionContext.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, codeExecutionContext, codeResponse); - } - - if (response == null || !response.Success) - { - _logger.LogWarning($"Failed to handle {msg}"); - return result; - } - - LogLevel logLevel; - if (response.Result.IsEqualTo("true")) - { - logLevel = LogLevel.Information; - result.Success = true; - result.IsValid = true; - } - else - { - logLevel = LogLevel.Warning; - } - - _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, $"Error when handling {msg}"); - return result; - } - } - - private List BuildArguments(string? name, JsonElement? args) - { - var keyValues = new List(); - if (args != null) - { - keyValues.Add(new KeyValue(name ?? "trigger_args", args.Value.GetRawText())); - } - return keyValues; - } -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 8b09f63b7..ebb2144ce 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,6 +1,4 @@ -using BotSharp.Abstraction.Templating; -using System.Data; -using System.Text.Json; +using System.Xml.Linq; namespace BotSharp.Core.Rules.Engines; @@ -41,18 +39,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - // Criteria validation - if (!string.IsNullOrEmpty(rule.RuleCriteria?.Name) && !rule.RuleCriteria.Disabled) - { - var criteriaResult = await ExecuteCriteriaAsync(agent, rule.RuleCriteria, trigger, text, states, options); - if (criteriaResult?.IsValid == false) - { - _logger.LogWarning("Criteria validation failed for agent {AgentId} with trigger {TriggerName}", agent.Id, trigger.Name); - continue; - } - } - - // Execute actions + // Execute graph // 1. Load graph (agent id, rule name) var graph = await LoadGraph(agent.Id, trigger); if (graph == null) @@ -68,7 +55,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te } // 3. Execute graph - var execResults = new List(); + var execResults = new List(); await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); var convIds = execResults.Where(x => x.Success && x.Data.TryGetValue("conversation_id", out _)) @@ -97,7 +84,7 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger MaxGraphRecursion = options.MaxGraphRecursion }; - var execResults = new List(); + var execResults = new List(); await ExecuteGraphNode( node, graph, agent, trigger, @@ -112,8 +99,14 @@ private async Task LoadGraph(string agentId, IRuleTrigger trigger, Ru var graph = RuleGraph.Init(); var root = new RuleNode { - Name = "root", - Type = "root", + Name = "start", + Type = "start", + }; + + var end = new RuleNode + { + Name = "end", + Type = "end", }; var delayNode = new RuleNode @@ -133,7 +126,7 @@ private async Task LoadGraph(string agentId, IRuleTrigger trigger, Ru Config = new() { ["http_method"] = "GET", - ["http_url"] = "https://dummy.restapiexample.com/api/v1/employees" + ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883958" } }; @@ -144,7 +137,7 @@ private async Task LoadGraph(string agentId, IRuleTrigger trigger, Ru Config = new() { ["http_method"] = "GET", - ["http_url"] = "https://dummy.restapiexample.com/api/v1/employee/1" + ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883956" } }; @@ -155,7 +148,7 @@ private async Task LoadGraph(string agentId, IRuleTrigger trigger, Ru Config = new() { ["http_method"] = "GET", - ["http_url"] = "https://dummy.restapiexample.com/api/v1/employee/2" + ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883954" } }; @@ -183,6 +176,12 @@ private async Task LoadGraph(string agentId, IRuleTrigger trigger, Ru Type = "is_next" }); + graph.AddEdge(node2, node3, payload: new() + { + Name = "edge", + Type = "is_next" + }); + return graph; } @@ -196,6 +195,12 @@ private async Task LoadDefaultGraph() Type = "root", }; + var end = new RuleNode + { + Name = "end", + Type = "end", + }; + var node = new RuleNode { Name = "send_message_to_agent", @@ -208,6 +213,12 @@ private async Task LoadDefaultGraph() Type = "is_next" }); + graph.AddEdge(node, end, payload: new() + { + Name = "edge", + Type = "is_next" + }); + return graph; } @@ -220,10 +231,11 @@ private async Task ExecuteGraphNode( string text, IEnumerable? states, RuleTriggerOptions? options, - List results) + List results) { + var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); var maxRecursion = options?.MaxGraphRecursion ?? RuleConstant.MAX_GRAPH_RECURSION; - if (results.Count >= maxRecursion) + if (actionResultCount >= maxRecursion) { _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", maxRecursion, agent.Name, trigger.Name); @@ -233,19 +245,13 @@ private async Task ExecuteGraphNode( var neighbors = graph.GetNeighbors(node); foreach (var (neighborNode, edge) in neighbors) { - if (!neighborNode.Type.IsEqualTo("action")) + if (RuleConstant.END_NODE_TYPES.Contains(neighborNode.Type)) { continue; } - var actions = _services.GetServices(); - var action = actions.FirstOrDefault(x => x.Name.IsEqualTo(neighborNode.Name)); - if (action == null) - { - continue; - } - - var context = new RuleActionContext + // Build context + var context = new RuleFlowContext { Node = neighborNode, Graph = graph, @@ -255,179 +261,232 @@ private async Task ExecuteGraphNode( JsonOptions = options?.JsonOptions }; - // Check whether the edge is executable from source node to target node - var isExecutable = await IsExecutable(edge, agent, trigger, context); - if (!isExecutable) - { - continue; - } - // Execute action - var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); - results.Add(new RuleActionStepResult + if (RuleConstant.CONDITION_NODE_TYPES.Contains(neighborNode.Type)) { - Node = neighborNode, - Success = actionResult.Success, - Response = actionResult.Response, - Data = new(actionResult.Data ?? []), - ErrorMessage = actionResult.ErrorMessage, - IsDelayed = actionResult.IsDelayed - }); - - if (results.Count >= maxRecursion) - { - _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", - maxRecursion, agent.Name, trigger.Name); - break; - } + // Execute condition + var conditionResult = await ExecuteCondition(neighborNode, graph, agent, trigger, context); + if (conditionResult == null) + { + continue; + } - if (actionResult.IsDelayed) - { - continue; + results.Add(RuleFlowStepResult.FromResult(conditionResult, neighborNode)); + + // If condition result is true, then execute the next node, otherwise skip + if (conditionResult.IsValid) + { + await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + } + else + { + _logger.LogInformation("Condition {ConditionName} evaluated to false, skipping next node (agent {Agent} and trigger {Trigger}).", + neighborNode.Name, agent.Name, trigger.Name); + } } + else if (RuleConstant.ACTION_NODE_TYPES.Contains(neighborNode.Type)) + { + // Execute action + var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); + if (actionResult == null) + { + continue; + } - await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); - } - } + results.Add(RuleFlowStepResult.FromResult(actionResult, neighborNode)); + if (actionResult.IsDelayed) + { + continue; + } - private async Task IsExecutable(RuleEdge edge, Agent agent, IRuleTrigger triger, RuleActionContext context) - { - return true; - } + actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); + if (actionResultCount >= maxRecursion) + { + _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", + maxRecursion, agent.Name, trigger.Name); + break; + } + await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + } + } + } - #region Criteria - private async Task ExecuteCriteriaAsync( + #region Action + private async Task ExecuteAction( + RuleNode node, + RuleGraph graph, Agent agent, - AgentRuleCriteria ruleCriteria, IRuleTrigger trigger, - string text, - IEnumerable? states, - RuleTriggerOptions? triggerOptions) + RuleFlowContext context) { - var result = new RuleCriteriaResult(); - try { - var criteria = _services.GetServices() - .FirstOrDefault(x => x.Provider == ruleCriteria.Name); - - if (criteria == null) + // Find the matching action + var foundAction = GetRuleAction(node, agent, trigger); + if (foundAction == null) { - return result; + var errorMsg = $"No rule action {node?.Name} is found"; + _logger.LogWarning(errorMsg); + return null; } - - var context = new RuleCriteriaContext - { - Text = text, - Parameters = BuildContextParameters(ruleCriteria.Config, states), - JsonOptions = triggerOptions?.JsonOptions - }; - - _logger.LogInformation("Start execution rule criteria {CriteriaProvider} for agent {AgentId} with trigger {TriggerName}", - criteria.Provider, agent.Id, trigger.Name); + _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", + foundAction.Name, agent.Id, trigger.Name); var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleCriteriaExecuted(agent, ruleCriteria, trigger, context); + await hook.BeforeRuleActionExecuted(agent, node, trigger, context); } - // Execute criteria + // Execute action context.Parameters ??= []; - result = await criteria.ValidateAsync(agent, trigger, context); + var result = await foundAction.ExecuteAsync(agent, trigger, context); foreach (var hook in hooks) { - await hook.AfterRuleCriteriaExecuted(agent, ruleCriteria, trigger, result); + await hook.AfterRuleActionExecuted(agent, node, trigger, result); } return result; } catch (Exception ex) { - _logger.LogError(ex, "Error executing rule criteria {CriteriaProvider} for agent {AgentId}", ruleCriteria.Name, agent.Id); - return result; + _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", node?.Name, agent.Id); + return new RuleNodeResult + { + Success = false, + ErrorMessage = ex.Message + }; } } + + // Find the matching action + private IRuleAction? GetRuleAction(RuleNode node, Agent agent, IRuleTrigger trigger) + { + var actions = _services.GetServices() + .Where(x => x.Name.IsEqualTo(node?.Name)) + .ToList(); + + var found = actions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id) && x.Triggers?.Contains(trigger.Name) == true); + if (found != null) + { + return found; + } + + found = actions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id)); + if (found != null) + { + return found; + } + + found = actions.FirstOrDefault(x => x.Triggers?.Contains(trigger.Name, StringComparer.OrdinalIgnoreCase) == true); + if (found != null) + { + return found; + } + + found = actions.FirstOrDefault(); + if (found != null) + { + return found; + } + + return null; + } #endregion - #region Action - private async Task ExecuteAction( + #region Condition + private async Task ExecuteCondition( RuleNode node, RuleGraph graph, Agent agent, IRuleTrigger trigger, - RuleActionContext context) + RuleFlowContext context) { try { - // Get all registered rule actions - var actions = _services.GetServices(); - - // Find the matching action - var foundAction = actions.FirstOrDefault(x => x.Name.IsEqualTo(node?.Name)); - - if (foundAction == null) + // Find the matching condition + var foundCondition = GetRuleCondition(node, agent, trigger); + if (foundCondition == null) { - var errorMsg = $"No rule action {node?.Name} is found"; + var errorMsg = $"No rule condition {node?.Name} is found"; _logger.LogWarning(errorMsg); - return RuleActionResult.Failed(errorMsg); + return null; } - _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", - foundAction.Name, agent.Id, trigger.Name); + _logger.LogInformation("Start execution rule condition {ConditionName} for agent {AgentId} with trigger {TriggerName}", + foundCondition.Name, agent.Id, trigger.Name); var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleActionExecuted(agent, node, trigger, context); + await hook.BeforeRuleConditionExecuted(agent, node, trigger, context); } - // Execute action + // Execute condition context.Parameters ??= []; - var result = await foundAction.ExecuteAsync(agent, trigger, context); + var result = await foundCondition.EvaluateAsync(agent, trigger, context); foreach (var hook in hooks) { - await hook.AfterRuleActionExecuted(agent, node, trigger, result); + await hook.AfterRuleConditionExecuted(agent, node, trigger, result); } return result; } catch (Exception ex) { - _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", node?.Name, agent.Id); - return RuleActionResult.Failed(ex.Message); + _logger.LogError(ex, "Error executing rule condition {ConditionName} for agent {AgentId}", node?.Name, agent.Id); + return new RuleNodeResult + { + Success = false, + IsValid = false, + ErrorMessage = ex.Message + }; } } - #endregion - - #region Private methods - private Dictionary BuildContextParameters(JsonDocument? config, IEnumerable? states) + // Find the matching condition + private IRuleCondition? GetRuleCondition(RuleNode node, Agent agent, IRuleTrigger trigger) { - var dict = new Dictionary(); + var conditions = _services.GetServices() + .Where(x => x.Name.IsEqualTo(node?.Name)) + .ToList(); - if (config != null) + var found = conditions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id) && x.Triggers?.Contains(trigger.Name) == true); + if (found != null) { - dict = ConvertToDictionary(config); + return found; } - if (!states.IsNullOrEmpty()) + found = conditions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id)); + if (found != null) { - foreach (var state in states!) - { - dict[state.Key] = state.Value?.ConvertToString(); - } + return found; } - return dict; + found = conditions.FirstOrDefault(x => x.Triggers?.Contains(trigger.Name, StringComparer.OrdinalIgnoreCase) == true); + if (found != null) + { + return found; + } + + found = conditions.FirstOrDefault(); + if (found != null) + { + return found; + } + + return null; } + #endregion + + #region Private methods private Dictionary BuildContextParameters(Dictionary? config, IEnumerable? states) { var dict = new Dictionary(); @@ -447,66 +506,5 @@ private async Task ExecuteAction( return dict; } - - private static Dictionary ConvertToDictionary(JsonDocument doc) - { - var dict = new Dictionary(); - - foreach (var prop in doc.RootElement.EnumerateObject()) - { - object? value = prop.Value.ValueKind switch - { - JsonValueKind.String => prop.Value.GetString(), - JsonValueKind.Number when prop.Value.TryGetDecimal(out decimal decimalValue) => decimalValue, - JsonValueKind.Number when prop.Value.TryGetDouble(out double doubleValue) => doubleValue, - JsonValueKind.Number when prop.Value.TryGetInt32(out int intValue) => intValue, - JsonValueKind.Number when prop.Value.TryGetInt64(out long longValue) => longValue, - JsonValueKind.Number when prop.Value.TryGetDateTime(out DateTime dateTimeValue) => dateTimeValue, - JsonValueKind.Number when prop.Value.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffsetValue) => dateTimeOffsetValue, - JsonValueKind.Number when prop.Value.TryGetGuid(out Guid guidValue) => guidValue, - JsonValueKind.Number => prop.Value.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => null, - JsonValueKind.Undefined => null, - JsonValueKind.Array => prop.Value, - JsonValueKind.Object => prop.Value, - _ => prop.Value - }; - dict[prop.Name] = value?.ConvertToString(); - } - - return dict; - #endregion - } - - private int? RenderSkippingExpression(string? expression, Dictionary dict) - { - int? steps = null; - - if (string.IsNullOrWhiteSpace(expression)) - { - return steps; - } - - var render = _services.GetRequiredService(); - var copy = dict != null - ? new Dictionary(dict.Where(x => x.Value != null).ToDictionary(x => x.Key, x => (object)x.Value!)) - : []; - var result = render.Render(expression, new Dictionary - { - { "states", copy } - }); - - if (int.TryParse(result, out var intVal)) - { - steps = intVal; - } - else if (bool.TryParse(result, out var boolVal) && boolVal) - { - steps = 1; - } - - return steps; - } + #endregion } diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index c12ce1f3c..4b4b5693f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,5 +1,5 @@ using BotSharp.Core.Rules.Actions; -using BotSharp.Core.Rules.Criteria; +using BotSharp.Core.Rules.Conditions; using BotSharp.Core.Rules.Engines; namespace BotSharp.Core.Rules; @@ -19,13 +19,16 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { services.AddScoped(); - services.AddScoped(); // Register rule actions services.AddScoped(); services.AddScoped(); services.AddScoped(); + // Register rule conditions + // Uncomment the line below to enable the example condition + // services.AddScoped(); + #if DEBUG services.AddScoped(); #endif diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index de5b21a14..959f03b02 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -16,24 +16,4 @@ public IEnumerable GetRuleTriggers() OutputArgs = x.OutputArgs }).OrderBy(x => x.TriggerName); } - - [HttpGet("/rule/criteria-providers")] - public async Task> GetRuleCriteriaProviders() - { - return _services.GetServices().OrderBy(x => x.Provider).Select(x => new KeyValue - { - Key = x.Provider, - Value = x.DefaultConfig != null ? x.DefaultConfig.RootElement.GetRawText() : "{}" - }); - } - - [HttpGet("/rule/actions")] - public async Task> GetRuleActions() - { - return _services.GetServices().OrderBy(x => x.Name).Select(x => new KeyValue - { - Key = x.Name, - Value = x.DefaultConfig != null ? x.DefaultConfig.RootElement.GetRawText() : "{}" - }); - } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index d1a3e538a..22d34f146 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using System.Text.Json; namespace BotSharp.Plugin.MongoStorage.Models; @@ -8,15 +7,13 @@ public class AgentRuleMongoElement { public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } - public AgentRuleCriteriaMongoModel? RuleCriteria { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { return new AgentRuleMongoElement { TriggerName = rule.TriggerName, - Disabled = rule.Disabled, - RuleCriteria = AgentRuleCriteriaMongoModel.ToMongoModel(rule.RuleCriteria) + Disabled = rule.Disabled }; } @@ -25,54 +22,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) return new AgentRule { TriggerName = rule.TriggerName, - Disabled = rule.Disabled, - RuleCriteria = AgentRuleCriteriaMongoModel.ToDomainModel(rule.RuleCriteria) + Disabled = rule.Disabled }; } } - -[BsonIgnoreExtraElements(Inherited = true)] -public class AgentRuleCriteriaMongoModel : AgentRuleConfigMongoModel -{ - public string CriteriaText { get; set; } - - public static AgentRuleCriteriaMongoModel? ToMongoModel(AgentRuleCriteria? criteria) - { - if (criteria == null) - { - return null; - } - - return new AgentRuleCriteriaMongoModel - { - Name = criteria.Name, - CriteriaText = criteria.CriteriaText, - Disabled = criteria.Disabled, - Config = criteria.Config != null ? BsonDocument.Parse(criteria.Config.RootElement.GetRawText()) : null - }; - } - - public static AgentRuleCriteria? ToDomainModel(AgentRuleCriteriaMongoModel? criteria) - { - if (criteria == null) - { - return null; - } - - return new AgentRuleCriteria - { - Name = criteria.Name, - CriteriaText = criteria.CriteriaText, - Disabled = criteria.Disabled, - Config = criteria.Config != null ? JsonDocument.Parse(criteria.Config.ToJson()) : null - }; - } -} - -[BsonIgnoreExtraElements(Inherited = true)] -public class AgentRuleConfigMongoModel -{ - public string Name { get; set; } - public bool Disabled { get; set; } - public BsonDocument? Config { get; set; } -} \ No newline at end of file From e6f1e9dda172e2a4309c5b8c49a76f0901195968 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 4 Mar 2026 15:53:48 -0600 Subject: [PATCH 62/91] add rule graph service --- .../Agents/Models/RuleGraph.cs | 10 +- .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 3 +- .../BotSharp.Abstraction/Rules/IRuleGraph.cs | 17 ++ .../Rules/Options/RuleGraphLoadOptions.cs | 8 + .../Rules/Options/RuleGraphOptions.cs | 19 ++ .../Rules/Options/RuleNodeExecutionOptions.cs | 3 +- .../Rules/Options/RuleTriggerOptions.cs | 4 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 229 +++++++----------- .../BotSharp.Core.Rules/RulesPlugin.cs | 8 +- .../GraphDb/MembaseGraphDb.cs | 1 + .../MembaseAuthHandler.cs | 2 +- .../{Services => Interfaces}/IMembaseApi.cs | 2 +- .../BotSharp.Plugin.Membase/MembasePlugin.cs | 7 + .../Services/DemoRuleGraph.cs | 119 +++++++++ .../Services/MembaseService.cs | 1 + 15 files changed, 273 insertions(+), 160 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs rename src/Plugins/BotSharp.Plugin.Membase/{Services => Handlers}/MembaseAuthHandler.cs (95%) rename src/Plugins/BotSharp.Plugin.Membase/{Services => Interfaces}/IMembaseApi.cs (97%) create mode 100644 src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 7a1f32bd1..1faf4fe81 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -18,8 +18,13 @@ public static RuleGraph Init() return new RuleGraph(); } - public RuleNode? GetRootNode() + public RuleNode? GetRootNode(string? name = null) { + if (!string.IsNullOrEmpty(name)) + { + return _nodes.FirstOrDefault(x => x.Name.IsEqualTo(name)); + } + return _nodes.FirstOrDefault(x => x.Type.IsEqualTo("root") || x.Type.IsEqualTo("start")); } @@ -145,7 +150,7 @@ public class RuleEdge : GraphItemPayload /// /// Edge type: is_next, etc. /// - public override string Type { get; set; } = "is_next"; + public override string Type { get; set; } = "next"; public RuleNode From { get; set; } public RuleNode To { get; set; } @@ -157,6 +162,7 @@ public RuleEdge() public RuleEdge(RuleNode from, RuleNode to) { + Id = Guid.NewGuid().ToString(); From = from; To = to; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index 8811bc358..d9958e549 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -21,8 +21,9 @@ Task> Triggered(IRuleTrigger trigger, string text, IEnumerab /// /// /// + /// /// /// /// - Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleNodeExecutionOptions options); + Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs new file mode 100644 index 000000000..3a84337d1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs @@ -0,0 +1,17 @@ +namespace BotSharp.Abstraction.Rules; + +public interface IRuleGraph +{ + /// + /// Rule graph provider + /// + string Provider { get; } + + /// + /// Load graph + /// + /// + /// + /// + Task LoadGraphAsync(string graphId, RuleGraphLoadOptions? options = null); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs new file mode 100644 index 000000000..895b8a44d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleGraphLoadOptions +{ + public string? AgentId { get; set; } + public string? Trigger { get; set; } + public IEnumerable? States { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs index 945a06c97..c39beba23 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs @@ -2,4 +2,23 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleGraphOptions { + /// + /// Graph provider + /// + public string Provider { get; set; } = string.Empty; + + /// + /// Graph id + /// + public string GraphId { get; set; } = string.Empty; + + /// + /// The name of the root node + /// + public string? RootNodeName { get; set; } + + /// + /// Max number of action node execution (prevent endless loop) + /// + public int? MaxGraphRecursion { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs index a6b4e9099..cdbb965d9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs @@ -2,8 +2,7 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleNodeExecutionOptions { - public string AgentId { get; set; } public string Text { get; set; } public IEnumerable States { get; set; } = []; - public int? MaxGraphRecursion { get; set; } + public RuleGraphOptions? GraphOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 90beff810..be98ddaf4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -16,7 +16,7 @@ public class RuleTriggerOptions public JsonSerializerOptions? JsonOptions { get; set; } /// - /// Max number of action node execution (prevent endless loop) + /// Rule graph options /// - public int? MaxGraphRecursion { get; set; } + public RuleGraphOptions? GraphOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index ebb2144ce..b0c1d7281 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.Xml.Linq; - namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -39,37 +37,47 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - // Execute graph - // 1. Load graph (agent id, rule name) - var graph = await LoadGraph(agent.Id, trigger); - if (graph == null) + if (!string.IsNullOrEmpty(options?.GraphOptions?.Provider) + && !string.IsNullOrEmpty(options?.GraphOptions?.GraphId)) { - continue; - } + // Execute graph + // 1. Load graph + var graph = await LoadGraph(options.GraphOptions.Provider, options.GraphOptions.GraphId, agent.Id, trigger, states); + if (graph == null) + { + continue; + } - // 2. Get root node - var root = graph.GetRootNode(); - if (root == null) - { - continue; - } + // 2. Get root node + var root = graph.GetRootNode(options.GraphOptions.RootNodeName); + if (root == null) + { + continue; + } - // 3. Execute graph - var execResults = new List(); - await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); + // 3. Execute graph + var execResults = new List(); + await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); - var convIds = execResults.Where(x => x.Success && x.Data.TryGetValue("conversation_id", out _)) - .Select(x => x.Data.GetValueOrDefault("conversation_id", string.Empty)) - .Where(x => !string.IsNullOrEmpty(x)) - .ToList(); + // Get conversation id to support legacy features + var convIds = execResults.Where(x => x.Success && x.Data.TryGetValue("conversation_id", out _)) + .Select(x => x.Data.GetValueOrDefault("conversation_id", string.Empty)) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); - newConversationIds.AddRange(convIds); + newConversationIds.AddRange(convIds); + } + else + { + var convId = await SendMessageToAgent(agent, trigger, text, states); + newConversationIds.Add(convId); + } } return newConversationIds; } - public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger trigger, RuleNodeExecutionOptions options) + public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options) { if (node == null || graph == null || options == null) { @@ -77,11 +85,11 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, IRuleTrigger } var agentService = _services.GetRequiredService(); - var agent = await agentService.GetAgent(options.AgentId); + var agent = await agentService.GetAgent(agentId); var triggerOptions = new RuleTriggerOptions { - MaxGraphRecursion = options.MaxGraphRecursion + GraphOptions = options.GraphOptions }; var execResults = new List(); @@ -94,135 +102,23 @@ await ExecuteGraphNode( execResults); } - private async Task LoadGraph(string agentId, IRuleTrigger trigger, RuleGraphOptions? options = null) - { - var graph = RuleGraph.Init(); - var root = new RuleNode - { - Name = "start", - Type = "start", - }; - - var end = new RuleNode - { - Name = "end", - Type = "end", - }; - - var delayNode = new RuleNode - { - Name = "delay_message", - Type = "action", - Config = new() - { - ["delay"] = "3 seconds" - } - }; - - var node1 = new RuleNode - { - Name = "http_request", - Type = "action", - Config = new() - { - ["http_method"] = "GET", - ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883958" - } - }; - - var node2 = new RuleNode - { - Name = "http_request", - Type = "action", - Config = new() - { - ["http_method"] = "GET", - ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883956" - } - }; - - var node3 = new RuleNode - { - Name = "http_request", - Type = "action", - Config = new() - { - ["http_method"] = "GET", - ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883954" - } - }; - - graph.AddEdge(root, delayNode, payload: new() - { - Name = "edge", - Type = "is_next" - }); - - graph.AddEdge(delayNode, node1, payload: new() - { - Name = "edge", - Type = "is_next" - }); - - graph.AddEdge(node1, node2, payload: new() - { - Name = "edge", - Type = "is_next" - }); - - graph.AddEdge(node1, node3, payload: new() - { - Name = "edge", - Type = "is_next" - }); - - graph.AddEdge(node2, node3, payload: new() - { - Name = "edge", - Type = "is_next" - }); - - return graph; - } - - - private async Task LoadDefaultGraph() + #region Graph + private async Task LoadGraph(string provider, string graphId, string agentId, IRuleTrigger trigger, IEnumerable? states) { - var graph = RuleGraph.Init(); - var root = new RuleNode - { - Name = "root", - Type = "root", - }; - - var end = new RuleNode - { - Name = "end", - Type = "end", - }; - - var node = new RuleNode - { - Name = "send_message_to_agent", - Type = "action" - }; - - graph.AddEdge(root, node, payload: new() + var graph = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + if (graph == null) { - Name = "edge", - Type = "is_next" - }); + return null; + } - graph.AddEdge(node, end, payload: new() + return await graph.LoadGraphAsync(graphId, options: new() { - Name = "edge", - Type = "is_next" + AgentId = agentId, + Trigger = trigger.Name, + States = states }); - - return graph; } - private async Task ExecuteGraphNode( RuleNode node, RuleGraph graph, @@ -234,7 +130,8 @@ private async Task ExecuteGraphNode( List results) { var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); - var maxRecursion = options?.MaxGraphRecursion ?? RuleConstant.MAX_GRAPH_RECURSION; + var maxRecursion = options?.GraphOptions?.MaxGraphRecursion ?? RuleConstant.MAX_GRAPH_RECURSION; + if (actionResultCount >= maxRecursion) { _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", @@ -312,6 +209,7 @@ private async Task ExecuteGraphNode( } } } + #endregion #region Action private async Task ExecuteAction( @@ -507,4 +405,39 @@ private async Task ExecuteGraphNode( return dict; } #endregion + + #region Legacy conversation + private async Task SendMessageToAgent(Agent agent, IRuleTrigger trigger, string text, IEnumerable? states = null) + { + 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.IsNullOrEmpty()) + { + 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/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 4b4b5693f..8457c275e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -18,6 +18,7 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { + // Register rule engine services.AddScoped(); // Register rule actions @@ -25,11 +26,12 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); - // Register rule conditions - // Uncomment the line below to enable the example condition - // services.AddScoped(); #if DEBUG + // Register rule conditions + services.AddScoped(); + + // Register rule trigger services.AddScoped(); #endif } diff --git a/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs b/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs index 1bcca821d..3744c8702 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs @@ -4,6 +4,7 @@ using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Utilities; +using BotSharp.Plugin.Membase.Interfaces; using Microsoft.Extensions.Logging; using System.Text.Json; diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseAuthHandler.cs b/src/Plugins/BotSharp.Plugin.Membase/Handlers/MembaseAuthHandler.cs similarity index 95% rename from src/Plugins/BotSharp.Plugin.Membase/Services/MembaseAuthHandler.cs rename to src/Plugins/BotSharp.Plugin.Membase/Handlers/MembaseAuthHandler.cs index 7dea503c8..c7b511f59 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseAuthHandler.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Handlers/MembaseAuthHandler.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Threading; -namespace BotSharp.Plugin.Membase.Services; +namespace BotSharp.Plugin.Membase.Handlers; public class MembaseAuthHandler : DelegatingHandler { diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/IMembaseApi.cs b/src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs similarity index 97% rename from src/Plugins/BotSharp.Plugin.Membase/Services/IMembaseApi.cs rename to src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs index 85c0b3b17..40180733e 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/IMembaseApi.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs @@ -1,7 +1,7 @@ using BotSharp.Plugin.Membase.Models.Graph; using Refit; -namespace BotSharp.Plugin.Membase.Services; +namespace BotSharp.Plugin.Membase.Interfaces; /// /// Membase REST API interface diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index 8a442ac4b..235b48fb4 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -1,6 +1,9 @@ using BotSharp.Abstraction.Graph; using BotSharp.Abstraction.Plugins.Models; +using BotSharp.Abstraction.Rules; using BotSharp.Plugin.Membase.GraphDb; +using BotSharp.Plugin.Membase.Handlers; +using BotSharp.Plugin.Membase.Interfaces; using Refit; namespace BotSharp.Plugin.Membase; @@ -33,6 +36,10 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) _membaseCredential = config.GetValue("Membase:ApiKey") ?? string.Empty; _membaseProjectId = config.GetValue("Membase:ProjectId") ?? string.Empty; + +#if DEBUG + services.AddScoped(); +#endif } public bool AttachMenu(List menu) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs new file mode 100644 index 000000000..b420fc99b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -0,0 +1,119 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Rules; +using BotSharp.Abstraction.Rules.Options; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.Membase.Services; + +public class DemoRuleGraph : IRuleGraph +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public DemoRuleGraph( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Provider => "demo"; + + public Task LoadGraphAsync(string graphId, RuleGraphLoadOptions? options = null) + { + var graph = RuleGraph.Init(); + var root = new RuleNode + { + Name = "start", + Type = "start", + }; + + var end = new RuleNode + { + Name = "end", + Type = "end", + }; + + var delayNode = new RuleNode + { + Name = "delay_message", + Type = "action", + Config = new() + { + ["delay"] = "3 seconds" + } + }; + + var node1 = new RuleNode + { + Name = "http_request", + Type = "action", + Config = new() + { + ["http_method"] = "GET", + ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883958" + } + }; + + var node2 = new RuleNode + { + Name = "http_request", + Type = "action", + Config = new() + { + ["http_method"] = "GET", + ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883956" + } + }; + + var node3 = new RuleNode + { + Name = "http_request", + Type = "action", + Config = new() + { + ["http_method"] = "GET", + ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883954" + } + }; + + graph.AddEdge(root, delayNode, payload: new() + { + Name = "edge", + Type = "is_next" + }); + + graph.AddEdge(delayNode, node1, payload: new() + { + Name = "edge", + Type = "next" + }); + + graph.AddEdge(node1, node2, payload: new() + { + Name = "edge", + Type = "next" + }); + + graph.AddEdge(node1, node3, payload: new() + { + Name = "edge", + Type = "next" + }); + + graph.AddEdge(node2, node3, payload: new() + { + Name = "edge", + Type = "next" + }); + + graph.AddEdge(node3, end, payload: new() + { + Name = "edge", + Type = "next" + }); + + return Task.FromResult(graph); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseService.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseService.cs index 380e9b703..d5f79cb1a 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseService.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseService.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Graph.Models; +using BotSharp.Plugin.Membase.Interfaces; using BotSharp.Plugin.Membase.Models.Graph; namespace BotSharp.Plugin.Membase.Services; From 23f2159304fff5690dff2f4c015067d6dbee8cbb Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 4 Mar 2026 15:57:48 -0600 Subject: [PATCH 63/91] split item and payload --- .../Agents/Models/RuleGraph.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 1faf4fe81..0b5aa22ef 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -132,7 +132,7 @@ public override string ToString() } } -public class RuleNode : GraphItemPayload +public class RuleNode : GraphItem { /// /// Node type: root, criteria, action, etc. @@ -145,7 +145,7 @@ public override string ToString() } } -public class RuleEdge : GraphItemPayload +public class RuleEdge : GraphItem { /// /// Edge type: is_next, etc. @@ -155,12 +155,12 @@ public class RuleEdge : GraphItemPayload public RuleNode From { get; set; } public RuleNode To { get; set; } - public RuleEdge() + public RuleEdge() : base() { } - public RuleEdge(RuleNode from, RuleNode to) + public RuleEdge(RuleNode from, RuleNode to) : base() { Id = Guid.NewGuid().ToString(); From = from; @@ -173,18 +173,17 @@ public override string ToString() } } -public class GraphItemPayload +public class GraphItem { public virtual string Id { get; set; } = Guid.NewGuid().ToString(); public virtual string Name { get; set; } = null!; public virtual string Type { get; set; } = null!; public virtual IEnumerable Labels { get; set; } = []; public virtual Dictionary Config { get; set; } = []; +} - public GraphItemPayload() - { - - } +public class GraphItemPayload : GraphItem +{ } public class RuleGraphInfo From bdf124ca018882f5326cd279fd47b52fbba0e792 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 4 Mar 2026 16:04:37 -0600 Subject: [PATCH 64/91] rename to GetGraphAsync --- src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs | 2 +- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 2 +- src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs index 3a84337d1..99d97b2c5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs @@ -13,5 +13,5 @@ public interface IRuleGraph /// /// /// - Task LoadGraphAsync(string graphId, RuleGraphLoadOptions? options = null); + Task GetGraphAsync(string graphId, RuleGraphLoadOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index b0c1d7281..739bf6fc1 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -111,7 +111,7 @@ await ExecuteGraphNode( return null; } - return await graph.LoadGraphAsync(graphId, options: new() + return await graph.GetGraphAsync(graphId, options: new() { AgentId = agentId, Trigger = trigger.Name, diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index b420fc99b..2b8d206f4 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -20,7 +20,7 @@ public DemoRuleGraph( public string Provider => "demo"; - public Task LoadGraphAsync(string graphId, RuleGraphLoadOptions? options = null) + public Task GetGraphAsync(string graphId, RuleGraphLoadOptions? options = null) { var graph = RuleGraph.Init(); var root = new RuleNode From 1b3f870445e5a7638df908b34dba7fd0268758de Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 4 Mar 2026 18:03:29 -0600 Subject: [PATCH 65/91] refine config --- .../Agents/Models/AgentRule.cs | 13 ++ .../Agents/Models/RuleGraph.cs | 3 + .../Rules/{IRuleGraph.cs => IRuleConfig.cs} | 10 +- ...oadOptions.cs => RuleConfigLoadOptions.cs} | 4 +- .../Rules/Options/RuleConfigOptions.cs | 19 ++ .../Rules/Options/RuleGraphOptions.cs | 24 --- .../Rules/Options/RuleNodeExecutionOptions.cs | 2 +- .../Rules/Options/RuleTriggerOptions.cs | 4 +- .../Rules/Settings/RuleSettings.cs | 9 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 48 +++-- .../BotSharp.Core.Rules/RulesPlugin.cs | 8 +- .../Controllers/Agent/AgentController.Rule.cs | 9 + .../GraphDb/MembaseGraphDb.cs | 2 +- .../BotSharp.Plugin.Membase/MembasePlugin.cs | 3 +- .../Services/DemoRuleGraph.cs | 189 +++++++++++++++++- .../Models/AgentRuleMongoElement.cs | 42 +++- src/WebStarter/appsettings.json | 8 + 17 files changed, 342 insertions(+), 55 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Rules/{IRuleGraph.cs => IRuleConfig.cs} (50%) rename src/Infrastructure/BotSharp.Abstraction/Rules/Options/{RuleGraphLoadOptions.cs => RuleConfigLoadOptions.cs} (57%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 8f9b6b2db..54f1e4068 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -7,4 +7,17 @@ public class AgentRule [JsonPropertyName("disabled")] public bool Disabled { get; set; } + + [JsonPropertyName("config")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RuleConfig? Config { get; set; } } + +public class RuleConfig +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("provider")] + public string? Provider { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 0b5aa22ef..f0b6bbd2d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -95,6 +95,8 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) Id = payload.Id, Name = payload.Name, Type = payload.Type, + Labels = payload.Labels, + Weight = payload.Weight, Config = payload.Config }); } @@ -179,6 +181,7 @@ public class GraphItem public virtual string Name { get; set; } = null!; public virtual string Type { get; set; } = null!; public virtual IEnumerable Labels { get; set; } = []; + public virtual int Weight { get; set; } public virtual Dictionary Config { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs similarity index 50% rename from src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs index 99d97b2c5..eed26fd24 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs @@ -1,17 +1,17 @@ namespace BotSharp.Abstraction.Rules; -public interface IRuleGraph +public interface IRuleConfig where T : class { /// - /// Rule graph provider + /// Rule config provider /// string Provider { get; } /// - /// Load graph + /// Load config /// - /// + /// /// /// - Task GetGraphAsync(string graphId, RuleGraphLoadOptions? options = null); + Task GetConfigAsync(string id, RuleConfigLoadOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigLoadOptions.cs similarity index 57% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigLoadOptions.cs index 895b8a44d..abb15b56a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphLoadOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigLoadOptions.cs @@ -1,8 +1,8 @@ namespace BotSharp.Abstraction.Rules.Options; -public class RuleGraphLoadOptions +public class RuleConfigLoadOptions { public string? AgentId { get; set; } public string? Trigger { get; set; } - public IEnumerable? States { get; set; } + public Dictionary? Parameters { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs new file mode 100644 index 000000000..efd72f545 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleConfigOptions +{ + /// + /// Config provider + /// + public string Provider { get; set; } = string.Empty; + + /// + /// Config id + /// + public string Id { get; set; } = string.Empty; + + /// + /// Additional custom parameters, e.g., root_node_name, max_recursion + /// + public Dictionary Parameters { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs deleted file mode 100644 index c39beba23..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleGraphOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Options; - -public class RuleGraphOptions -{ - /// - /// Graph provider - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Graph id - /// - public string GraphId { get; set; } = string.Empty; - - /// - /// The name of the root node - /// - public string? RootNodeName { get; set; } - - /// - /// Max number of action node execution (prevent endless loop) - /// - public int? MaxGraphRecursion { get; set; } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs index cdbb965d9..14b1d85ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs @@ -4,5 +4,5 @@ public class RuleNodeExecutionOptions { public string Text { get; set; } public IEnumerable States { get; set; } = []; - public RuleGraphOptions? GraphOptions { get; set; } + public RuleConfigOptions? ConfigOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index be98ddaf4..1263723e3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -16,7 +16,7 @@ public class RuleTriggerOptions public JsonSerializerOptions? JsonOptions { get; set; } /// - /// Rule graph options + /// Rule config options /// - public RuleGraphOptions? GraphOptions { get; set; } + public RuleConfigOptions? ConfigOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs new file mode 100644 index 000000000..f21edee19 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Abstraction.Rules.Settings; + +public class RuleSettings +{ + /// + /// [type] => [providers], e.g., ["graph"] => ["graph provider 1", "graph provider 2"] + /// + public Dictionary> ConfigOptions { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 739bf6fc1..3a5db4a6a 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -37,19 +37,24 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - if (!string.IsNullOrEmpty(options?.GraphOptions?.Provider) - && !string.IsNullOrEmpty(options?.GraphOptions?.GraphId)) + var ruleConfig = rule.Config; + var ruleConfigProvider = options?.ConfigOptions?.Provider ?? ruleConfig?.Provider; + + if (ruleConfig != null + && ruleConfig.Type.IsEqualTo("graph") + && !string.IsNullOrEmpty(ruleConfigProvider)) { // Execute graph // 1. Load graph - var graph = await LoadGraph(options.GraphOptions.Provider, options.GraphOptions.GraphId, agent.Id, trigger, states); + var graph = await LoadGraph(ruleConfigProvider, options?.ConfigOptions?.Id, agent.Id, trigger, states); if (graph == null) { continue; } // 2. Get root node - var root = graph.GetRootNode(options.GraphOptions.RootNodeName); + var rootNodeName = options.ConfigOptions.Parameters.GetValueOrDefault("root_node_name"); + var root = graph.GetRootNode(rootNodeName); if (root == null) { continue; @@ -89,7 +94,7 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentI var triggerOptions = new RuleTriggerOptions { - GraphOptions = options.GraphOptions + ConfigOptions = options.ConfigOptions }; var execResults = new List(); @@ -105,17 +110,35 @@ await ExecuteGraphNode( #region Graph private async Task LoadGraph(string provider, string graphId, string agentId, IRuleTrigger trigger, IEnumerable? states) { - var graph = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); - if (graph == null) + var config = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + if (config == null) { return null; } - return await graph.GetGraphAsync(graphId, options: new() + if (string.IsNullOrEmpty(graphId)) + { + return null; + } + + var param = new Dictionary(); + if (!states.IsNullOrEmpty()) + { + foreach (var state in states!) + { + if (state.Key == null || state.Value == null) + { + continue; + } + param[state.Key] = state.Value; + } + } + + return await config.GetConfigAsync(graphId, options: new() { AgentId = agentId, Trigger = trigger.Name, - States = states + Parameters = param }); } @@ -130,7 +153,8 @@ private async Task ExecuteGraphNode( List results) { var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); - var maxRecursion = options?.GraphOptions?.MaxGraphRecursion ?? RuleConstant.MAX_GRAPH_RECURSION; + var param = options?.ConfigOptions?.Parameters ?? []; + var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion"), out var depth) ? depth : RuleConstant.MAX_GRAPH_RECURSION; if (actionResultCount >= maxRecursion) { @@ -153,7 +177,7 @@ private async Task ExecuteGraphNode( Node = neighborNode, Graph = graph, Text = text, - Parameters = BuildContextParameters(neighborNode.Config, states), + Parameters = BuildParameters(neighborNode.Config, states), PrevStepResults = results, JsonOptions = options?.JsonOptions }; @@ -385,7 +409,7 @@ private async Task ExecuteGraphNode( #region Private methods - private Dictionary BuildContextParameters(Dictionary? config, IEnumerable? states) + private Dictionary BuildParameters(Dictionary? config, IEnumerable? states) { var dict = new Dictionary(); diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 8457c275e..569bc4555 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Crontab.Settings; +using BotSharp.Abstraction.Rules.Settings; using BotSharp.Core.Rules.Actions; using BotSharp.Core.Rules.Conditions; using BotSharp.Core.Rules.Engines; @@ -18,6 +20,11 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { + // Register rule settings + var settings = new RuleSettings(); + config.Bind("Rule", settings); + services.AddSingleton(settings); + // Register rule engine services.AddScoped(); @@ -26,7 +33,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); - #if DEBUG // Register rule conditions services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 959f03b02..bae16e387 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Rules; +using BotSharp.Abstraction.Rules.Settings; namespace BotSharp.OpenAPI.Controllers; @@ -16,4 +17,12 @@ public IEnumerable GetRuleTriggers() OutputArgs = x.OutputArgs }).OrderBy(x => x.TriggerName); } + + [HttpGet("/rule/config/options")] + public IDictionary> GetRuleConfigOptions() + { + var settings = _services.GetRequiredService(); + var options = settings?.ConfigOptions ?? []; + return options; + } } diff --git a/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs b/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs index 3744c8702..30209eba5 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/GraphDb/MembaseGraphDb.cs @@ -39,7 +39,7 @@ public async Task ExecuteQueryAsync(string query, GraphQueryEx try { - var response = await _membaseApi.CypherQueryAsync(options.GraphId, new CypherQueryRequest + var response = await _membaseApi.CypherQueryAsync(options!.GraphId, new CypherQueryRequest { Query = query, Parameters = args diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index 235b48fb4..eb11522a2 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Graph; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Rules; @@ -38,7 +39,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) _membaseProjectId = config.GetValue("Membase:ProjectId") ?? string.Empty; #if DEBUG - services.AddScoped(); + services.AddScoped, DemoRuleGraph>(); #endif } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 2b8d206f4..4e8fa8341 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -1,11 +1,15 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Graph; +using BotSharp.Abstraction.Graph.Models; using BotSharp.Abstraction.Rules; using BotSharp.Abstraction.Rules.Options; +using BotSharp.Abstraction.Utilities; using Microsoft.Extensions.Logging; +using System.Text.Json; namespace BotSharp.Plugin.Membase.Services; -public class DemoRuleGraph : IRuleGraph +public class DemoRuleGraph : IRuleConfig { private readonly IServiceProvider _services; private readonly ILogger _logger; @@ -18,9 +22,186 @@ public DemoRuleGraph( _logger = logger; } - public string Provider => "demo"; + public string Provider => "membase"; - public Task GetGraphAsync(string graphId, RuleGraphLoadOptions? options = null) + public async Task GetConfigAsync(string id, RuleConfigLoadOptions? options = null) + { + if (string.IsNullOrEmpty(id)) + { + return null; + } + + var query = $""" + MATCH (a)-[r]->(b) + WITH DISTINCT a, r, b + RETURN a, r, b + LIMIT 100 + """; + + var args = new Dictionary(); + if (options?.Parameters != null) + { + foreach (var param in options.Parameters!) + { + if (param.Key == null || param.Value == null) + { + continue; + } + args[param.Key] = param.Value; + } + } + + if (options?.AgentId != null) + { + args["agent_id"] = options.AgentId; + } + + if (options?.Trigger != null) + { + args["trigger"] = options.Trigger; + } + + try + { + var graphDb = _services.GetServices().First(x => x.Provider.IsEqualTo(Provider)); + var result = await graphDb.ExecuteQueryAsync(query, options: new() + { + GraphId = id, + Arguments = args + }); + + if (result == null) + { + return null; + } + + var graph = BuildGraph(result); + return graph; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error when loading graph (id: {GraphId}) for agent {AgentId} and trigger {Trigger} ", + id, options?.AgentId, options?.Trigger); + return null; + } + } + + private RuleGraph BuildGraph(GraphQueryResult result) + { + var graph = RuleGraph.Init(); + if (result.Values.IsNullOrEmpty()) + { + return graph; + } + + foreach (var item in result.Values) + { + // Try to deserialize nodes and edge from the dictionary + if (!item.TryGetValue("a", out var sourceNodeElement) || + !item.TryGetValue("b", out var targetNodeElement) || + !item.TryGetValue("r", out var edgeElement)) + { + continue; + } + + // Parse source node + var sourceNodeId = sourceNodeElement.GetProperty("id").GetString(); + var sourceNodeLabels = sourceNodeElement.TryGetProperty("labels", out var sLabels) + ? sLabels.EnumerateArray().Select(x => x.GetString() ?? "").ToList() + : []; + var sourceNodeProps = sourceNodeElement.TryGetProperty("properties", out var sProps) + ? sProps + : default; + + // Parse target node + var targetNodeId = targetNodeElement.GetProperty("id").GetString(); + var targetNodeLabels = targetNodeElement.TryGetProperty("labels", out var tLabels) + ? tLabels.EnumerateArray().Select(x => x.GetString() ?? "").ToList() + : []; + var targetNodeProps = targetNodeElement.TryGetProperty("properties", out var tProps) + ? tProps + : default; + + // Parse edge + var edgeId = edgeElement.GetProperty("id").GetString(); + var edgeProps = edgeElement.TryGetProperty("properties", out var eProps) + ? eProps + : default; + var edgeWeight = edgeElement.TryGetProperty("weight", out var eWeight) && eWeight.ValueKind == JsonValueKind.Number + ? (int)eWeight.GetDouble() + : 1; + + // Create source node + var sourceNode = new RuleNode() + { + Id = sourceNodeId ?? Guid.NewGuid().ToString(), + Labels = sourceNodeLabels, + Name = GetGraphItemAttribute(sourceNodeProps, key: "name", defaultValue: "node"), + Type = GetGraphItemAttribute(sourceNodeProps, key: "type", defaultValue: "action"), + Config = GetConfig(sourceNodeProps) + }; + + // Create target node + var targetNode = new RuleNode() + { + Id = targetNodeId ?? Guid.NewGuid().ToString(), + Labels = targetNodeLabels, + Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "node"), + Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "action"), + Config = GetConfig(targetNodeProps) + }; + + // Create edge payload + var edgePayload = new GraphItemPayload() + { + Id = edgeId ?? Guid.NewGuid().ToString(), + Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "edge"), + Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "next"), + Weight = edgeWeight, + Config = GetConfig(edgeProps) + }; + + // Add edge to graph + graph.AddEdge(sourceNode, targetNode, edgePayload); + } + + return graph; + } + + private string GetGraphItemAttribute(JsonElement? properties, string key, string defaultValue) + { + if (properties == null || properties.Value.ValueKind == JsonValueKind.Undefined) + { + return defaultValue; + } + + if (properties.Value.TryGetProperty(key, out var name) && name.ValueKind == JsonValueKind.String) + { + return name.GetString() ?? defaultValue; + } + + return defaultValue; + } + + private Dictionary GetConfig(JsonElement? properties) + { + var config = new Dictionary(); + + if (properties == null || properties.Value.ValueKind == JsonValueKind.Undefined) + { + return config; + } + + // Convert all properties to config dictionary + foreach (var prop in properties.Value.EnumerateObject()) + { + config[prop.Name] = prop.Value.ConvertToString(); + } + + return config; + } + + private RuleGraph GetDefaultGraph() { var graph = RuleGraph.Init(); var root = new RuleNode @@ -114,6 +295,6 @@ public Task GetGraphAsync(string graphId, RuleGraphLoadOptions? optio Type = "next" }); - return Task.FromResult(graph); + return graph; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 22d34f146..0903099be 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -7,13 +7,15 @@ public class AgentRuleMongoElement { public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } + public RuleConfigMongoModel? Config { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { return new AgentRuleMongoElement { TriggerName = rule.TriggerName, - Disabled = rule.Disabled + Disabled = rule.Disabled, + Config = RuleConfigMongoModel.ToMongoModel(rule.Config) }; } @@ -22,7 +24,43 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) return new AgentRule { TriggerName = rule.TriggerName, - Disabled = rule.Disabled + Disabled = rule.Disabled, + Config = RuleConfigMongoModel.ToDomainModel(rule.Config) + }; + } +} + +[BsonIgnoreExtraElements(Inherited = true)] +public class RuleConfigMongoModel +{ + public string? Type { get; set; } + public string? Provider { get; set; } + + public static RuleConfigMongoModel? ToMongoModel(RuleConfig? config) + { + if (config == null) + { + return null; + } + + return new RuleConfigMongoModel + { + Type = config.Type, + Provider = config.Provider + }; + } + + public static RuleConfig? ToDomainModel(RuleConfigMongoModel? config) + { + if (config == null) + { + return null; + } + + return new RuleConfig + { + Type = config.Type, + Provider = config.Provider }; } } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index b3c2b35e9..0996487f5 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -736,6 +736,14 @@ "Enabled": false }, + "Rules": { + "ConfigOptions": { + "Graph": [ + "membase" + ] + } + }, + "Crontab": { "Watcher": { "Enabled": false From aa4a10898a9895d8b14e8cecd312500ed385abf9 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 4 Mar 2026 21:53:54 -0600 Subject: [PATCH 66/91] refine --- .../BotSharp.Abstraction/Rules/IRuleConfig.cs | 4 +- .../Rules/Models/RuleFlowContext.cs | 1 + .../Rules/Options/RuleNodeExecutionOptions.cs | 3 ++ .../Constants/RuleConstant.cs | 1 - .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 46 +++++++++++-------- .../Services/DemoRuleGraph.cs | 4 +- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs index eed26fd24..5e1d5a0ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs @@ -8,10 +8,10 @@ public interface IRuleConfig where T : class string Provider { get; } /// - /// Load config + /// Get rule topology /// /// /// /// - Task GetConfigAsync(string id, RuleConfigLoadOptions? options = null); + Task GetTopologyAsync(string id, RuleConfigLoadOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs index 7fd0f7a18..4e9040efe 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs @@ -12,6 +12,7 @@ public class RuleFlowContext public IEnumerable PrevStepResults { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } public RuleNode Node { get; set; } + public RuleEdge Edge { get; set; } public RuleGraph Graph { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs index 14b1d85ec..2a2b150c1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs @@ -1,8 +1,11 @@ +using System.Text.Json; + namespace BotSharp.Abstraction.Rules.Options; public class RuleNodeExecutionOptions { public string Text { get; set; } public IEnumerable States { get; set; } = []; + public JsonSerializerOptions? JsonOptions { get; set; } public RuleConfigOptions? ConfigOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index a7a773a96..fa634ab94 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -2,7 +2,6 @@ namespace BotSharp.Core.Rules.Constants; public static class RuleConstant { - public const string DEFAULT_CRITERIA_PROVIDER = "code_script"; public const int MAX_GRAPH_RECURSION = 10; public static IEnumerable CONDITION_NODE_TYPES = new List diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 3a5db4a6a..c2da089d9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -39,21 +39,21 @@ public async Task> Triggered(IRuleTrigger trigger, string te var ruleConfig = rule.Config; var ruleConfigProvider = options?.ConfigOptions?.Provider ?? ruleConfig?.Provider; + var ruleConfigId = options?.ConfigOptions?.Id; - if (ruleConfig != null - && ruleConfig.Type.IsEqualTo("graph") - && !string.IsNullOrEmpty(ruleConfigProvider)) + if (!string.IsNullOrEmpty(ruleConfigProvider)) { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleConfigProvider, options?.ConfigOptions?.Id, agent.Id, trigger, states); + var graph = await LoadGraph(ruleConfigProvider, ruleConfigId, agent.Id, trigger, states); if (graph == null) { continue; } // 2. Get root node - var rootNodeName = options.ConfigOptions.Parameters.GetValueOrDefault("root_node_name"); + var param = options?.ConfigOptions?.Parameters; + var rootNodeName = param != null ? param.GetValueOrDefault("root_node_name") : null; var root = graph.GetRootNode(rootNodeName); if (root == null) { @@ -94,7 +94,8 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentI var triggerOptions = new RuleTriggerOptions { - ConfigOptions = options.ConfigOptions + ConfigOptions = options.ConfigOptions, + JsonOptions = options.JsonOptions }; var execResults = new List(); @@ -116,11 +117,6 @@ await ExecuteGraphNode( return null; } - if (string.IsNullOrEmpty(graphId)) - { - return null; - } - var param = new Dictionary(); if (!states.IsNullOrEmpty()) { @@ -134,7 +130,7 @@ await ExecuteGraphNode( } } - return await config.GetConfigAsync(graphId, options: new() + return await config.GetTopologyAsync(graphId, options: new() { AgentId = agentId, Trigger = trigger.Name, @@ -152,9 +148,10 @@ private async Task ExecuteGraphNode( RuleTriggerOptions? options, List results) { + // Check whether the action nodes have been visited more than limit var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); var param = options?.ConfigOptions?.Parameters ?? []; - var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion"), out var depth) ? depth : RuleConstant.MAX_GRAPH_RECURSION; + var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion"), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; if (actionResultCount >= maxRecursion) { @@ -163,18 +160,21 @@ private async Task ExecuteGraphNode( return; } + // Get current node neighbors var neighbors = graph.GetNeighbors(node); - foreach (var (neighborNode, edge) in neighbors) + if (neighbors.IsNullOrEmpty()) { - if (RuleConstant.END_NODE_TYPES.Contains(neighborNode.Type)) - { - continue; - } + return; + } + // Visit neighbor nodes + foreach (var (neighborNode, edge) in neighbors) + { // Build context var context = new RuleFlowContext { Node = neighborNode, + Edge = edge, Graph = graph, Text = text, Parameters = BuildParameters(neighborNode.Config, states), @@ -185,7 +185,7 @@ private async Task ExecuteGraphNode( if (RuleConstant.CONDITION_NODE_TYPES.Contains(neighborNode.Type)) { - // Execute condition + // Execute condition node var conditionResult = await ExecuteCondition(neighborNode, graph, agent, trigger, context); if (conditionResult == null) { @@ -207,7 +207,7 @@ private async Task ExecuteGraphNode( } else if (RuleConstant.ACTION_NODE_TYPES.Contains(neighborNode.Type)) { - // Execute action + // Execute action node var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); if (actionResult == null) { @@ -231,10 +231,15 @@ private async Task ExecuteGraphNode( await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); } + else + { + await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + } } } #endregion + #region Action private async Task ExecuteAction( RuleNode node, @@ -430,6 +435,7 @@ private async Task ExecuteGraphNode( } #endregion + #region Legacy conversation private async Task SendMessageToAgent(Agent agent, IRuleTrigger trigger, string text, IEnumerable? states = null) { diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 4e8fa8341..13b0d0ff0 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -24,11 +24,11 @@ public DemoRuleGraph( public string Provider => "membase"; - public async Task GetConfigAsync(string id, RuleConfigLoadOptions? options = null) + public async Task GetTopologyAsync(string id, RuleConfigLoadOptions? options = null) { if (string.IsNullOrEmpty(id)) { - return null; + return GetDefaultGraph(); } var query = $""" From b92308a6dc9104bdc0f0facfaaf523105960b965 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 4 Mar 2026 21:59:49 -0600 Subject: [PATCH 67/91] rename to IRuleFlow --- .../Rules/{IRuleConfig.cs => IRuleFlow.cs} | 8 ++-- .../Rules/Options/RuleCriteriaOptions.cs | 39 ------------------- ...gLoadOptions.cs => RuleFlowLoadOptions.cs} | 2 +- ...uleConfigOptions.cs => RuleFlowOptions.cs} | 2 +- .../Rules/Options/RuleNodeExecutionOptions.cs | 2 +- .../Rules/Options/RuleTriggerOptions.cs | 4 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 20 +++++----- .../BotSharp.Plugin.Membase/MembasePlugin.cs | 2 +- .../Services/DemoRuleGraph.cs | 4 +- 9 files changed, 22 insertions(+), 61 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/Rules/{IRuleConfig.cs => IRuleFlow.cs} (57%) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs rename src/Infrastructure/BotSharp.Abstraction/Rules/Options/{RuleConfigLoadOptions.cs => RuleFlowLoadOptions.cs} (84%) rename src/Infrastructure/BotSharp.Abstraction/Rules/Options/{RuleConfigOptions.cs => RuleFlowOptions.cs} (93%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs similarity index 57% rename from src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs index 5e1d5a0ec..22bac7618 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs @@ -1,17 +1,17 @@ namespace BotSharp.Abstraction.Rules; -public interface IRuleConfig where T : class +public interface IRuleFlow where T : class { /// - /// Rule config provider + /// Rule flow provider /// string Provider { get; } /// - /// Get rule topology + /// Get rule flow topology /// /// /// /// - Task GetTopologyAsync(string id, RuleConfigLoadOptions? options = null); + Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs deleted file mode 100644 index 27b5167f4..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleCriteriaOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -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; } - - /// - /// Custom parameters - /// - public Dictionary Parameters { get; set; } = []; -} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigLoadOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs similarity index 84% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigLoadOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs index abb15b56a..72a1fb05b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigLoadOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Rules.Options; -public class RuleConfigLoadOptions +public class RuleFlowLoadOptions { public string? AgentId { get; set; } public string? Trigger { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs similarity index 93% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index efd72f545..cd563eb89 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleConfigOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Rules.Options; -public class RuleConfigOptions +public class RuleFlowOptions { /// /// Config provider diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs index 2a2b150c1..b2bbd2214 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs @@ -7,5 +7,5 @@ public class RuleNodeExecutionOptions public string Text { get; set; } public IEnumerable States { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } - public RuleConfigOptions? ConfigOptions { get; set; } + public RuleFlowOptions? FlowOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 1263723e3..06e9cb135 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -16,7 +16,7 @@ public class RuleTriggerOptions public JsonSerializerOptions? JsonOptions { get; set; } /// - /// Rule config options + /// Rule flow options /// - public RuleConfigOptions? ConfigOptions { get; set; } + public RuleFlowOptions? FlowOptions { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index c2da089d9..67be33c6c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -38,21 +38,21 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var ruleConfig = rule.Config; - var ruleConfigProvider = options?.ConfigOptions?.Provider ?? ruleConfig?.Provider; - var ruleConfigId = options?.ConfigOptions?.Id; + var ruleFlowProvider = options?.FlowOptions?.Provider ?? ruleConfig?.Provider; + var ruleFlowId = options?.FlowOptions?.Id; - if (!string.IsNullOrEmpty(ruleConfigProvider)) + if (!string.IsNullOrEmpty(ruleFlowProvider)) { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleConfigProvider, ruleConfigId, agent.Id, trigger, states); + var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent.Id, trigger, states); if (graph == null) { continue; } // 2. Get root node - var param = options?.ConfigOptions?.Parameters; + var param = options?.FlowOptions?.Parameters; var rootNodeName = param != null ? param.GetValueOrDefault("root_node_name") : null; var root = graph.GetRootNode(rootNodeName); if (root == null) @@ -94,7 +94,7 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentI var triggerOptions = new RuleTriggerOptions { - ConfigOptions = options.ConfigOptions, + FlowOptions = options.FlowOptions, JsonOptions = options.JsonOptions }; @@ -111,8 +111,8 @@ await ExecuteGraphNode( #region Graph private async Task LoadGraph(string provider, string graphId, string agentId, IRuleTrigger trigger, IEnumerable? states) { - var config = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); - if (config == null) + var flow = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + if (flow == null) { return null; } @@ -130,7 +130,7 @@ await ExecuteGraphNode( } } - return await config.GetTopologyAsync(graphId, options: new() + return await flow.GetTopologyAsync(graphId, options: new() { AgentId = agentId, Trigger = trigger.Name, @@ -150,7 +150,7 @@ private async Task ExecuteGraphNode( { // Check whether the action nodes have been visited more than limit var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); - var param = options?.ConfigOptions?.Parameters ?? []; + var param = options?.FlowOptions?.Parameters ?? []; var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion"), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; if (actionResultCount >= maxRecursion) diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index eb11522a2..e04154eda 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -39,7 +39,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) _membaseProjectId = config.GetValue("Membase:ProjectId") ?? string.Empty; #if DEBUG - services.AddScoped, DemoRuleGraph>(); + services.AddScoped, DemoRuleGraph>(); #endif } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 13b0d0ff0..881c3f884 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -9,7 +9,7 @@ namespace BotSharp.Plugin.Membase.Services; -public class DemoRuleGraph : IRuleConfig +public class DemoRuleGraph : IRuleFlow { private readonly IServiceProvider _services; private readonly ILogger _logger; @@ -24,7 +24,7 @@ public DemoRuleGraph( public string Provider => "membase"; - public async Task GetTopologyAsync(string id, RuleConfigLoadOptions? options = null) + public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null) { if (string.IsNullOrEmpty(id)) { From 97150ffc0b63ac2161ae12b49076e1680a575619 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 4 Mar 2026 22:04:08 -0600 Subject: [PATCH 68/91] minor change --- .../Rules/Options/RuleFlowOptions.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 23 ++++--------------- .../Services/DemoRuleGraph.cs | 20 ++++++++-------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index cd563eb89..83d4429a8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -15,5 +15,5 @@ public class RuleFlowOptions /// /// Additional custom parameters, e.g., root_node_name, max_recursion /// - public Dictionary Parameters { get; set; } = []; + public Dictionary Parameters { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 67be33c6c..891e7589c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -45,7 +45,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent.Id, trigger, states); + var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent.Id, trigger, options?.FlowOptions?.Parameters); if (graph == null) { continue; @@ -53,7 +53,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // 2. Get root node var param = options?.FlowOptions?.Parameters; - var rootNodeName = param != null ? param.GetValueOrDefault("root_node_name") : null; + var rootNodeName = param != null ? param.GetValueOrDefault("root_node_name")?.ToString() : null; var root = graph.GetRootNode(rootNodeName); if (root == null) { @@ -109,7 +109,7 @@ await ExecuteGraphNode( } #region Graph - private async Task LoadGraph(string provider, string graphId, string agentId, IRuleTrigger trigger, IEnumerable? states) + private async Task LoadGraph(string provider, string graphId, string agentId, IRuleTrigger trigger, Dictionary? parameters) { var flow = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); if (flow == null) @@ -117,24 +117,11 @@ await ExecuteGraphNode( return null; } - var param = new Dictionary(); - if (!states.IsNullOrEmpty()) - { - foreach (var state in states!) - { - if (state.Key == null || state.Value == null) - { - continue; - } - param[state.Key] = state.Value; - } - } - return await flow.GetTopologyAsync(graphId, options: new() { AgentId = agentId, Trigger = trigger.Name, - Parameters = param + Parameters = new(parameters ?? []) }); } @@ -151,7 +138,7 @@ private async Task ExecuteGraphNode( // Check whether the action nodes have been visited more than limit var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); var param = options?.FlowOptions?.Parameters ?? []; - var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion"), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; + var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; if (actionResultCount >= maxRecursion) { diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 881c3f884..42a0b565c 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -39,6 +39,16 @@ LIMIT 100 """; var args = new Dictionary(); + if (options?.AgentId != null) + { + args["agent_id"] = options.AgentId; + } + + if (options?.Trigger != null) + { + args["trigger"] = options.Trigger; + } + if (options?.Parameters != null) { foreach (var param in options.Parameters!) @@ -51,16 +61,6 @@ LIMIT 100 } } - if (options?.AgentId != null) - { - args["agent_id"] = options.AgentId; - } - - if (options?.Trigger != null) - { - args["trigger"] = options.Trigger; - } - try { var graphDb = _services.GetServices().First(x => x.Provider.IsEqualTo(Provider)); From 89f5204d8d6006a702354962955fc3e862687f3e Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 4 Mar 2026 22:38:54 -0600 Subject: [PATCH 69/91] rename --- .../BotSharp.Abstraction/Agents/Models/AgentRule.cs | 4 ++-- .../Rules/Options/RuleFlowOptions.cs | 6 +++--- .../Rules/Options/RuleNodeExecutionOptions.cs | 2 +- .../Rules/Options/RuleTriggerOptions.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 12 ++++++------ .../Models/AgentRuleMongoElement.cs | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 54f1e4068..52685044c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -15,8 +15,8 @@ public class AgentRule public class RuleConfig { - [JsonPropertyName("type")] - public string? Type { get; set; } + [JsonPropertyName("topology")] + public string? Topology { get; set; } [JsonPropertyName("provider")] public string? Provider { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index 83d4429a8..abcad3632 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -3,14 +3,14 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleFlowOptions { /// - /// Config provider + /// Flow provider /// public string Provider { get; set; } = string.Empty; /// - /// Config id + /// Flow topology id /// - public string Id { get; set; } = string.Empty; + public string TopologyId { get; set; } = string.Empty; /// /// Additional custom parameters, e.g., root_node_name, max_recursion diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs index b2bbd2214..2bc5b5c52 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleNodeExecutionOptions.cs @@ -7,5 +7,5 @@ public class RuleNodeExecutionOptions public string Text { get; set; } public IEnumerable States { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } - public RuleFlowOptions? FlowOptions { get; set; } + public RuleFlowOptions? Flow { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 06e9cb135..46e35fe86 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -18,5 +18,5 @@ public class RuleTriggerOptions /// /// Rule flow options /// - public RuleFlowOptions? FlowOptions { get; set; } + public RuleFlowOptions? Flow { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 891e7589c..4bb919a64 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -38,21 +38,21 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var ruleConfig = rule.Config; - var ruleFlowProvider = options?.FlowOptions?.Provider ?? ruleConfig?.Provider; - var ruleFlowId = options?.FlowOptions?.Id; + var ruleFlowProvider = options?.Flow?.Provider ?? ruleConfig?.Provider; + var ruleFlowId = options?.Flow?.TopologyId; if (!string.IsNullOrEmpty(ruleFlowProvider)) { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent.Id, trigger, options?.FlowOptions?.Parameters); + var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent.Id, trigger, options?.Flow?.Parameters); if (graph == null) { continue; } // 2. Get root node - var param = options?.FlowOptions?.Parameters; + var param = options?.Flow?.Parameters; var rootNodeName = param != null ? param.GetValueOrDefault("root_node_name")?.ToString() : null; var root = graph.GetRootNode(rootNodeName); if (root == null) @@ -94,7 +94,7 @@ public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentI var triggerOptions = new RuleTriggerOptions { - FlowOptions = options.FlowOptions, + Flow = options.Flow, JsonOptions = options.JsonOptions }; @@ -137,7 +137,7 @@ private async Task ExecuteGraphNode( { // Check whether the action nodes have been visited more than limit var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); - var param = options?.FlowOptions?.Parameters ?? []; + var param = options?.Flow?.Parameters ?? []; var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; if (actionResultCount >= maxRecursion) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 0903099be..ff8442c01 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -33,7 +33,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) [BsonIgnoreExtraElements(Inherited = true)] public class RuleConfigMongoModel { - public string? Type { get; set; } + public string? Topology { get; set; } public string? Provider { get; set; } public static RuleConfigMongoModel? ToMongoModel(RuleConfig? config) @@ -45,7 +45,7 @@ public class RuleConfigMongoModel return new RuleConfigMongoModel { - Type = config.Type, + Topology = config.Topology, Provider = config.Provider }; } @@ -59,7 +59,7 @@ public class RuleConfigMongoModel return new RuleConfig { - Type = config.Type, + Topology = config.Topology, Provider = config.Provider }; } From 0e65f41152910528a9368f879d84dae66f3bf498 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 5 Mar 2026 14:48:36 -0600 Subject: [PATCH 70/91] refine membase query --- .../Agents/Models/RuleGraph.cs | 2 +- .../Rules/Options/RuleFlowLoadOptions.cs | 2 - .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 13 +- .../Controllers/MembaseController.cs | 332 +++++++++++++++++- .../Interfaces/IMembaseApi.cs | 4 +- .../Services/DemoRuleGraph.cs | 24 +- 6 files changed, 349 insertions(+), 28 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index f0b6bbd2d..ff592a091 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -181,7 +181,7 @@ public class GraphItem public virtual string Name { get; set; } = null!; public virtual string Type { get; set; } = null!; public virtual IEnumerable Labels { get; set; } = []; - public virtual int Weight { get; set; } + public virtual double Weight { get; set; } = 1.0; public virtual Dictionary Config { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs index 72a1fb05b..c8e7c515d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs @@ -2,7 +2,5 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleFlowLoadOptions { - public string? AgentId { get; set; } - public string? Trigger { get; set; } public Dictionary? Parameters { get; set; } } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 4bb919a64..f266a3677 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -45,7 +45,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent.Id, trigger, options?.Flow?.Parameters); + var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent, trigger, options?.Flow?.Parameters); if (graph == null) { continue; @@ -109,7 +109,7 @@ await ExecuteGraphNode( } #region Graph - private async Task LoadGraph(string provider, string graphId, string agentId, IRuleTrigger trigger, Dictionary? parameters) + private async Task LoadGraph(string provider, string graphId, Agent agent, IRuleTrigger trigger, Dictionary? parameters) { var flow = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); if (flow == null) @@ -117,11 +117,14 @@ await ExecuteGraphNode( return null; } + var param = new Dictionary(parameters ?? []); + param["agent"] = param.GetValueOrDefault("agent", agent.Name); + param["agent_id"] = param.GetValueOrDefault("agent_id", agent.Id); + param["trigger"] = param.GetValueOrDefault("trigger", trigger.Name); + return await flow.GetTopologyAsync(graphId, options: new() { - AgentId = agentId, - Trigger = trigger.Name, - Parameters = new(parameters ?? []) + Parameters = param }); } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs b/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs index dbc7393a8..8ab4ff3e8 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Graph; +using BotSharp.Plugin.Membase.Interfaces; using Microsoft.AspNetCore.Http; namespace BotSharp.Plugin.Membase.Controllers; @@ -8,11 +9,43 @@ namespace BotSharp.Plugin.Membase.Controllers; public class MembaseController : ControllerBase { private readonly IServiceProvider _services; + private readonly IMembaseApi _membaseApi; public MembaseController( - IServiceProvider services) + IServiceProvider services, + IMembaseApi membaseApi) { _services = services; + _membaseApi = membaseApi; + } + + /// + /// Get graph information + /// + /// The graph identifier + /// Graph information + [HttpGet("/membase/{graphId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetGraphInfo(string graphId) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + try + { + var graphInfo = await _membaseApi.GetGraphInfoAsync(graphId); + return Ok(graphInfo); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while retrieving graph information.", error = ex.Message }); + } } /// @@ -58,4 +91,301 @@ public async Task ExecuteGraphQuery(string graphId, [FromBody] Cy new { message = "An error occurred while executing the query.", error = ex.Message }); } } + + /// + /// Get a node from the graph + /// + /// The graph identifier + /// The node identifier + /// The node + [HttpGet("/membase/{graphId}/node/{nodeId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetNode(string graphId, string nodeId) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(nodeId)) + { + return BadRequest("Node ID cannot be empty."); + } + + try + { + var node = await _membaseApi.GetNodeAsync(graphId, nodeId); + return Ok(node); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while retrieving the node.", error = ex.Message }); + } + } + + /// + /// Create a node in the graph + /// + /// The graph identifier + /// The node creation model + /// The created node + [HttpPost("/membase/{graphId}/node")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateNode(string graphId, [FromBody] NodeCreationModel request) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (request == null) + { + return BadRequest("Node creation model cannot be null."); + } + + try + { + var node = await _membaseApi.CreateNodeAsync(graphId, request); + return Ok(node); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while creating the node.", error = ex.Message }); + } + } + + /// + /// Merge a node in the graph + /// + /// The graph identifier + /// The node identifier + /// The node update model + /// The merged node + [HttpPut("/membase/{graphId}/node/{nodeId}/merge")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task MergeNode(string graphId, string nodeId, [FromBody] NodeUpdateModel request) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(nodeId)) + { + return BadRequest("Node ID cannot be empty."); + } + + try + { + var node = await _membaseApi.MergeNodeAsync(graphId, nodeId, request); + return Ok(node); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while merging the node.", error = ex.Message }); + } + } + + /// + /// Delete a node from the graph + /// + /// The graph identifier + /// The node identifier + /// Delete response + [HttpDelete("/membase/{graphId}/node/{nodeId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteNode(string graphId, string nodeId) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(nodeId)) + { + return BadRequest("Node ID cannot be empty."); + } + + try + { + await _membaseApi.DeleteNodeAsync(graphId, nodeId); + return Ok("done"); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while deleting the node.", error = ex.Message }); + } + } + + /// + /// Get an edge from the graph + /// + /// The graph identifier + /// The edge identifier + /// The edge + [HttpGet("/membase/{graphId}/edge/{edgeId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task GetEdge(string graphId, string edgeId) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(edgeId)) + { + return BadRequest("Edge ID cannot be empty."); + } + + try + { + var edge = await _membaseApi.GetEdgeAsync(graphId, edgeId); + return Ok(edge); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while retrieving the edge.", error = ex.Message }); + } + } + + /// + /// Create an edge in the graph + /// + /// The graph identifier + /// The edge creation model + /// The created edge + [HttpPost("/membase/{graphId}/edge")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateEdge(string graphId, [FromBody] EdgeCreationModel request) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (request == null) + { + return BadRequest("Edge creation model cannot be null."); + } + + if (string.IsNullOrWhiteSpace(request.SourceNodeId)) + { + return BadRequest("Source node ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(request.TargetNodeId)) + { + return BadRequest("Target node ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(request.Type)) + { + return BadRequest("Edge type cannot be empty."); + } + + try + { + var edge = await _membaseApi.CreateEdgeAsync(graphId, request); + return Ok(edge); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while creating the edge.", error = ex.Message }); + } + } + + /// + /// Update an edge in the graph + /// + /// The graph identifier + /// The edge identifier + /// The edge update model + /// The updated edge + [HttpPut("/membase/{graphId}/edge/{edgeId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task UpdateEdge(string graphId, string edgeId, [FromBody] EdgeUpdateModel request) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(edgeId)) + { + return BadRequest("Edge ID cannot be empty."); + } + + try + { + var edge = await _membaseApi.UpdateEdgeAsync(graphId, edgeId, request); + return Ok(edge); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while updating the edge.", error = ex.Message }); + } + } + + /// + /// Delete an edge from the graph + /// + /// The graph identifier + /// The edge identifier + /// Delete response + [HttpDelete("/membase/{graphId}/edge/{edgeId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task DeleteEdge(string graphId, string edgeId) + { + if (string.IsNullOrWhiteSpace(graphId)) + { + return BadRequest("Graph ID cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(edgeId)) + { + return BadRequest("Edge ID cannot be empty."); + } + + try + { + await _membaseApi.DeleteEdgeAsync(graphId, edgeId); + return Ok("done"); + } + catch (Exception ex) + { + return StatusCode( + StatusCodes.Status500InternalServerError, + new { message = "An error occurred while deleting the edge.", error = ex.Message }); + } + } } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs b/src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs index 40180733e..1c294060a 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Interfaces/IMembaseApi.cs @@ -29,7 +29,7 @@ public interface IMembaseApi Task MergeNodeAsync(string graphId, string nodeId, [Body] NodeUpdateModel node); [Delete("/graph/{graphId}/node/{nodeId}")] - Task DeleteNodeAsync(string graphId, string nodeId); + Task DeleteNodeAsync(string graphId, string nodeId); #endregion #region Edge @@ -43,6 +43,6 @@ public interface IMembaseApi Task UpdateEdgeAsync(string graphId, string edgeId, [Body] EdgeUpdateModel edge); [Delete("/graph/{graphId}/edge/{edgeId}")] - Task DeleteEdgeAsync(string graphId, string edgeId); + Task DeleteEdgeAsync(string graphId, string edgeId); #endregion } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 42a0b565c..e4a80b80c 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -22,7 +22,7 @@ public DemoRuleGraph( _logger = logger; } - public string Provider => "membase"; + public string Provider => "demo"; public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null) { @@ -33,22 +33,13 @@ public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? op var query = $""" MATCH (a)-[r]->(b) - WITH DISTINCT a, r, b + WITH a, r, b + WHERE a.agent = $agent AND a.trigger = $trigger AND b.agent = $agent AND b.trigger = $trigger RETURN a, r, b LIMIT 100 """; var args = new Dictionary(); - if (options?.AgentId != null) - { - args["agent_id"] = options.AgentId; - } - - if (options?.Trigger != null) - { - args["trigger"] = options.Trigger; - } - if (options?.Parameters != null) { foreach (var param in options.Parameters!) @@ -63,7 +54,7 @@ LIMIT 100 try { - var graphDb = _services.GetServices().First(x => x.Provider.IsEqualTo(Provider)); + var graphDb = _services.GetServices().First(x => x.Provider.IsEqualTo("membase")); var result = await graphDb.ExecuteQueryAsync(query, options: new() { GraphId = id, @@ -80,8 +71,7 @@ LIMIT 100 } catch (Exception ex) { - _logger.LogError(ex, "Error when loading graph (id: {GraphId}) for agent {AgentId} and trigger {Trigger} ", - id, options?.AgentId, options?.Trigger); + _logger.LogError(ex, "Error when loading graph (id: {GraphId})", id); return null; } } @@ -128,8 +118,8 @@ private RuleGraph BuildGraph(GraphQueryResult result) ? eProps : default; var edgeWeight = edgeElement.TryGetProperty("weight", out var eWeight) && eWeight.ValueKind == JsonValueKind.Number - ? (int)eWeight.GetDouble() - : 1; + ? eWeight.GetDouble() + : 1.0; // Create source node var sourceNode = new RuleNode() From 2b7eb3b0b92b2fda0453da537cbb96ba00725e78 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 5 Mar 2026 15:06:20 -0600 Subject: [PATCH 71/91] refine --- .../BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs | 1 - .../BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs | 5 ----- .../BotSharp.Core.Rules/Actions/HttpRuleAction.cs | 3 +++ .../BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs | 3 --- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs | 3 +-- 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs index 5acf337b6..b4b6d5953 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs @@ -13,7 +13,6 @@ public static RuleFlowStepResult FromResult(RuleNodeResult result, RuleNode node { Node = node, Success = result.Success, - IsValid = result.IsValid, Response = result.Response, ErrorMessage = result.ErrorMessage, Data = new(result.Data ?? []), diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs index a9020f284..113126f0d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleNodeResult.cs @@ -7,11 +7,6 @@ public class RuleNodeResult /// public virtual bool Success { get; set; } - /// - /// Whether the node evaluation is valid (used for conditions) - /// - public virtual bool IsValid { get; set; } - /// /// Response content from the node /// diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index 403faa333..36fef4161 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -1,5 +1,7 @@ using System.Net.Mime; +using System.Text.Json; using System.Web; +using System.Xml.Linq; namespace BotSharp.Core.Rules.Actions; @@ -82,6 +84,7 @@ public async Task ExecuteAsync( Response = responseContent, Data = new() { + ["http_response_headers"] = JsonSerializer.Serialize(response.Headers), ["http_response"] = responseContent } }; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs index 24af33062..e34666a41 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs @@ -42,7 +42,6 @@ public async Task EvaluateAsync( return new RuleNodeResult { Success = true, - IsValid = true, Response = $"Condition met: {parameterName} = {actualValue}" }; } @@ -51,7 +50,6 @@ public async Task EvaluateAsync( return new RuleNodeResult { Success = true, - IsValid = false, Response = $"Condition not met: {parameterName} = {actualValue}, expected = {expectedValue}" }; } @@ -62,7 +60,6 @@ public async Task EvaluateAsync( return new RuleNodeResult { Success = false, - IsValid = false, ErrorMessage = ex.Message }; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index f266a3677..450244198 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -185,7 +185,7 @@ private async Task ExecuteGraphNode( results.Add(RuleFlowStepResult.FromResult(conditionResult, neighborNode)); // If condition result is true, then execute the next node, otherwise skip - if (conditionResult.IsValid) + if (conditionResult.Success) { await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); } @@ -361,7 +361,6 @@ private async Task ExecuteGraphNode( return new RuleNodeResult { Success = false, - IsValid = false, ErrorMessage = ex.Message }; } From 4cccd62e8aeb1d5df46526f063c91b7835dcd7ef Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 5 Mar 2026 15:42:47 -0600 Subject: [PATCH 72/91] refine --- .../Agents/Models/RuleGraph.cs | 6 ++-- .../Actions/HttpRuleAction.cs | 1 - .../Conditions/ExampleRuleCondition.cs | 4 +-- .../Constants/RuleConstant.cs | 2 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 32 ++++++++++++------- .../Services/DemoRuleGraph.cs | 3 ++ 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index ff592a091..682f9e29c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -97,6 +97,7 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) Type = payload.Type, Labels = payload.Labels, Weight = payload.Weight, + Purpose = payload.Purpose, Config = payload.Config }); } @@ -143,7 +144,7 @@ public class RuleNode : GraphItem public override string ToString() { - return $"Node ({Id}): {Name} ({Type})"; + return $"Node ({Id}): {Name} ({Type} => {Purpose})"; } } @@ -171,7 +172,7 @@ public RuleEdge(RuleNode from, RuleNode to) : base() public override string ToString() { - return $"Edge ({Id}): {Name} ({Type}), Connects from Node ({From?.Name}) to Node ({To?.Name})"; + return $"Edge ({Id}): {Name} ({Type} => {Purpose}), Connects from Node ({From?.Name}) to Node ({To?.Name})"; } } @@ -182,6 +183,7 @@ public class GraphItem public virtual string Type { get; set; } = null!; public virtual IEnumerable Labels { get; set; } = []; public virtual double Weight { get; set; } = 1.0; + public virtual string? Purpose { get; set; } public virtual Dictionary Config { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs index 36fef4161..402f098a9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRuleAction.cs @@ -1,7 +1,6 @@ using System.Net.Mime; using System.Text.Json; using System.Web; -using System.Xml.Linq; namespace BotSharp.Core.Rules.Actions; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs index e34666a41..92410991e 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs @@ -41,7 +41,7 @@ public async Task EvaluateAsync( { return new RuleNodeResult { - Success = true, + Success = isMatch, Response = $"Condition met: {parameterName} = {actualValue}" }; } @@ -49,7 +49,7 @@ public async Task EvaluateAsync( { return new RuleNodeResult { - Success = true, + Success = isMatch, Response = $"Condition not met: {parameterName} = {actualValue}, expected = {expectedValue}" }; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index fa634ab94..aed9fd2f2 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -2,7 +2,7 @@ namespace BotSharp.Core.Rules.Constants; public static class RuleConstant { - public const int MAX_GRAPH_RECURSION = 10; + public const int MAX_GRAPH_RECURSION = 30; public static IEnumerable CONDITION_NODE_TYPES = new List { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 450244198..b1cc2d791 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Mvc; + namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -139,11 +141,12 @@ private async Task ExecuteGraphNode( List results) { // Check whether the action nodes have been visited more than limit - var actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); + var visited = results.Count(); var param = options?.Flow?.Parameters ?? []; - var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; + var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 + ? depth : RuleConstant.MAX_GRAPH_RECURSION; - if (actionResultCount >= maxRecursion) + if (visited >= maxRecursion) { _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", maxRecursion, agent.Name, trigger.Name); @@ -179,6 +182,11 @@ private async Task ExecuteGraphNode( var conditionResult = await ExecuteCondition(neighborNode, graph, agent, trigger, context); if (conditionResult == null) { + results.Add(RuleFlowStepResult.FromResult(new() + { + Success = false, + ErrorMessage = $"Unable to find condition {neighborNode.Name}." + }, neighborNode)); continue; } @@ -201,6 +209,11 @@ private async Task ExecuteGraphNode( var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); if (actionResult == null) { + results.Add(RuleFlowStepResult.FromResult(new() + { + Success = false, + ErrorMessage = $"Unable to find action {neighborNode.Name}." + }, neighborNode)); continue; } @@ -211,18 +224,15 @@ private async Task ExecuteGraphNode( continue; } - actionResultCount = results.Count(x => RuleConstant.ACTION_NODE_TYPES.Contains(x.Node.Type)); - if (actionResultCount >= maxRecursion) - { - _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", - maxRecursion, agent.Name, trigger.Name); - break; - } - await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); } else { + results.Add(RuleFlowStepResult.FromResult(new() + { + Success = true, + Response = $"Pass through node {neighborNode.Name}." + }, neighborNode)); await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); } } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index e4a80b80c..975caeaea 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -128,6 +128,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Labels = sourceNodeLabels, Name = GetGraphItemAttribute(sourceNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(sourceNodeProps, key: "type", defaultValue: "action"), + Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "empty"), Config = GetConfig(sourceNodeProps) }; @@ -138,6 +139,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Labels = targetNodeLabels, Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "action"), + Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "empty"), Config = GetConfig(targetNodeProps) }; @@ -147,6 +149,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Id = edgeId ?? Guid.NewGuid().ToString(), Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "edge"), Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "next"), + Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "empty"), Weight = edgeWeight, Config = GetConfig(edgeProps) }; From 0650f1142eebd8c7993f942b39282d05862f2ef4 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 5 Mar 2026 17:45:26 -0600 Subject: [PATCH 73/91] add rule config --- .../BotSharp.Abstraction/Rules/IRuleConfig.cs | 10 ++++++ .../Rules/Settings/RuleSettings.cs | 4 --- .../Controllers/Agent/AgentController.Rule.cs | 16 ++++++--- .../BotSharp.Plugin.Membase/MembasePlugin.cs | 1 + .../Services/DemoRuleConfig.cs | 33 +++++++++++++++++++ 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs create mode 100644 src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs new file mode 100644 index 000000000..13961394f --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace BotSharp.Abstraction.Rules; + +public interface IRuleConfig +{ + string Provider { get; } + + Task GetConfigAsync(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs index f21edee19..defa0aa84 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs @@ -2,8 +2,4 @@ namespace BotSharp.Abstraction.Rules.Settings; public class RuleSettings { - /// - /// [type] => [providers], e.g., ["graph"] => ["graph provider 1", "graph provider 2"] - /// - public Dictionary> ConfigOptions { get; set; } = []; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index bae16e387..c00919db2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Rules; -using BotSharp.Abstraction.Rules.Settings; namespace BotSharp.OpenAPI.Controllers; @@ -19,10 +18,17 @@ public IEnumerable GetRuleTriggers() } [HttpGet("/rule/config/options")] - public IDictionary> GetRuleConfigOptions() + public async Task> GetRuleConfigOptions() { - var settings = _services.GetRequiredService(); - var options = settings?.ConfigOptions ?? []; - return options; + var dict = new Dictionary(); + var configs = _services.GetServices(); + + foreach (var config in configs) + { + var json = await config.GetConfigAsync(); + dict[config.Provider.ToLower()] = json; + } + + return dict; } } diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index e04154eda..756e282b3 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -40,6 +40,7 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) #if DEBUG services.AddScoped, DemoRuleGraph>(); + services.AddScoped(); #endif } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs new file mode 100644 index 000000000..641f30f3b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs @@ -0,0 +1,33 @@ +using BotSharp.Abstraction.Rules; +using System.Text.Json; + +namespace BotSharp.Plugin.Membase.Services; + +public class DemoRuleConfig : IRuleConfig +{ + private readonly IServiceProvider _services; + + public DemoRuleConfig( + IServiceProvider services) + { + _services = services; + } + + public string Provider => "membase"; + + public async Task GetConfigAsync() + { + var settings = _services.GetRequiredService(); + var apiKey = settings.ApiKey; + var projectId = "68503047c5796a8049634a51"; + var graphId = "69a76a0ea77b9871345de795"; + var query = "MATCH%20(a)-[r]-%3E(b)%20WITH%20a,%20r,%20b%20WHERE%20a.agent%20=%20$agent%20AND%20a.trigger%20=%20$trigger%20AND%20b.agent%20=%20$agent%20AND%20b.trigger%20=%20$trigger%20RETURN%20a,%20r,%20b%20LIMIT%20100"; + + return JsonDocument.Parse(JsonSerializer.Serialize(new + { + source = "membase", + htmlTag = "iframe", + url = $"https://console.membase.dev/query-editor/{projectId}?graphId={graphId}&query={query}&token={apiKey}" + })); + } +} From b7819b01b09751dd2242b49ce4cdaf88137d2de5 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 6 Mar 2026 11:53:20 -0600 Subject: [PATCH 74/91] refine topology config --- .../Agents/Models/AgentRule.cs | 7 +- .../Agents/Models/RuleGraph.cs | 9 ++- .../Rules/Hooks/IRuleTriggerHook.cs | 4 +- .../BotSharp.Abstraction/Rules/IRuleConfig.cs | 10 --- .../BotSharp.Abstraction/Rules/IRuleFlow.cs | 8 +++ .../Rules/Models/RuleConfigModel.cs | 10 +++ .../Rules/Options/RuleFlowLoadOptions.cs | 3 + .../Rules/Options/RuleFlowOptions.cs | 9 ++- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 71 ++++++++++--------- .../Controllers/Agent/AgentController.Rule.cs | 14 ++-- .../BotSharp.Plugin.Membase/MembasePlugin.cs | 1 - .../Services/DemoRuleConfig.cs | 33 --------- .../Services/DemoRuleGraph.cs | 40 +++++++++-- .../Settings/MembaseSettings.cs | 19 +++++ .../Models/AgentRuleMongoElement.cs | 9 +-- 15 files changed, 142 insertions(+), 105 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs delete mode 100644 src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 52685044c..6bcdf78ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -15,9 +15,6 @@ public class AgentRule public class RuleConfig { - [JsonPropertyName("topology")] - public string? Topology { get; set; } - - [JsonPropertyName("provider")] - public string? Provider { get; set; } + [JsonPropertyName("topology_provider")] + public string? TopologyProvider { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 682f9e29c..9364b5dab 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -103,7 +103,14 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) } } - public IEnumerable<(RuleNode, RuleEdge)> GetNeighbors(RuleNode node) + public IEnumerable<(RuleNode, RuleEdge)> GetParentNodes(RuleNode node) + { + return _edges.Where(e => e.To != null && e.To.Id.IsEqualTo(node.Id)) + .Select(e => (e.From, e)) + .ToList(); + } + + public IEnumerable<(RuleNode, RuleEdge)> GetChildrenNodes(RuleNode node) { return _edges.Where(e => e.From != null && e.From.Id.IsEqualTo(node.Id)) .Select(e => (e.To, e)) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index 101a191fc..c2ed36bb4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -5,9 +5,9 @@ namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { - Task BeforeRuleConditionExecuted(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; + Task BeforeRuleConditionExecuting(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; Task AfterRuleConditionExecuted(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleNodeResult result) => Task.CompletedTask; - Task BeforeRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; + Task BeforeRuleActionExecuting(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleNodeResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs deleted file mode 100644 index 13961394f..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json; - -namespace BotSharp.Abstraction.Rules; - -public interface IRuleConfig -{ - string Provider { get; } - - Task GetConfigAsync(); -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs index 22bac7618..37b0126cb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Rules.Models; + namespace BotSharp.Abstraction.Rules; public interface IRuleFlow where T : class @@ -7,6 +9,12 @@ public interface IRuleFlow where T : class /// string Provider { get; } + /// + /// Get rule flow topology config + /// + /// + Task GetTopologyConfigAsync(); + /// /// Get rule flow topology /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs new file mode 100644 index 000000000..f105a66d3 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace BotSharp.Abstraction.Rules.Models; + +public class RuleConfigModel +{ + public string TopologyId { get; set; } + public string TopologyProvider { get; set; } + public JsonDocument CustomConfig { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs index c8e7c515d..31705e6bb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs @@ -2,5 +2,8 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleFlowLoadOptions { + public string? AgentId { get; set; } + public string? TriggerName { get; set; } + public string? Query { get; set; } public Dictionary? Parameters { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index abcad3632..4da1c8335 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -3,15 +3,20 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleFlowOptions { /// - /// Flow provider + /// Flow topology provider /// - public string Provider { get; set; } = string.Empty; + public string TopologyProvider { get; set; } = string.Empty; /// /// Flow topology id /// public string TopologyId { get; set; } = string.Empty; + /// + /// Query to get flow topology + /// + public string? Query { get; set; } + /// /// Additional custom parameters, e.g., root_node_name, max_recursion /// diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index b1cc2d791..bf8ae7b58 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Mvc; - namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -40,14 +38,13 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var ruleConfig = rule.Config; - var ruleFlowProvider = options?.Flow?.Provider ?? ruleConfig?.Provider; - var ruleFlowId = options?.Flow?.TopologyId; + var ruleFlowProvider = options?.Flow?.TopologyProvider ?? ruleConfig?.TopologyProvider; if (!string.IsNullOrEmpty(ruleFlowProvider)) { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleFlowProvider, ruleFlowId, agent, trigger, options?.Flow?.Parameters); + var graph = await LoadGraph(ruleFlowProvider, agent, trigger, options?.Flow); if (graph == null) { continue; @@ -111,7 +108,7 @@ await ExecuteGraphNode( } #region Graph - private async Task LoadGraph(string provider, string graphId, Agent agent, IRuleTrigger trigger, Dictionary? parameters) + private async Task LoadGraph(string provider, Agent agent, IRuleTrigger trigger, RuleFlowOptions? options) { var flow = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); if (flow == null) @@ -119,13 +116,23 @@ await ExecuteGraphNode( return null; } - var param = new Dictionary(parameters ?? []); + var param = new Dictionary(options?.Parameters ?? []); param["agent"] = param.GetValueOrDefault("agent", agent.Name); param["agent_id"] = param.GetValueOrDefault("agent_id", agent.Id); param["trigger"] = param.GetValueOrDefault("trigger", trigger.Name); - return await flow.GetTopologyAsync(graphId, options: new() + var topologyId = options?.TopologyId; + if (string.IsNullOrEmpty(topologyId)) + { + var config = await flow.GetTopologyConfigAsync(); + topologyId = config.TopologyId; + } + + return await flow.GetTopologyAsync(topologyId, options: new() { + AgentId = agent.Id, + TriggerName = trigger.Name, + Query = options?.Query, Parameters = param }); } @@ -153,87 +160,87 @@ private async Task ExecuteGraphNode( return; } - // Get current node neighbors - var neighbors = graph.GetNeighbors(node); - if (neighbors.IsNullOrEmpty()) + // Get current node successors + var nextNodes = graph.GetChildrenNodes(node); + if (nextNodes.IsNullOrEmpty()) { return; } // Visit neighbor nodes - foreach (var (neighborNode, edge) in neighbors) + foreach (var (nextNode, edge) in nextNodes) { // Build context var context = new RuleFlowContext { - Node = neighborNode, + Node = nextNode, Edge = edge, Graph = graph, Text = text, - Parameters = BuildParameters(neighborNode.Config, states), + Parameters = BuildParameters(nextNode.Config, states), PrevStepResults = results, JsonOptions = options?.JsonOptions }; - if (RuleConstant.CONDITION_NODE_TYPES.Contains(neighborNode.Type)) + if (RuleConstant.CONDITION_NODE_TYPES.Contains(nextNode.Type)) { // Execute condition node - var conditionResult = await ExecuteCondition(neighborNode, graph, agent, trigger, context); + var conditionResult = await ExecuteCondition(nextNode, graph, agent, trigger, context); if (conditionResult == null) { results.Add(RuleFlowStepResult.FromResult(new() { Success = false, - ErrorMessage = $"Unable to find condition {neighborNode.Name}." - }, neighborNode)); + ErrorMessage = $"Unable to find condition {nextNode.Name}." + }, nextNode)); continue; } - results.Add(RuleFlowStepResult.FromResult(conditionResult, neighborNode)); + results.Add(RuleFlowStepResult.FromResult(conditionResult, nextNode)); // If condition result is true, then execute the next node, otherwise skip if (conditionResult.Success) { - await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNode(nextNode, graph, agent, trigger, text, states, options, results); } else { _logger.LogInformation("Condition {ConditionName} evaluated to false, skipping next node (agent {Agent} and trigger {Trigger}).", - neighborNode.Name, agent.Name, trigger.Name); + nextNode.Name, agent.Name, trigger.Name); } } - else if (RuleConstant.ACTION_NODE_TYPES.Contains(neighborNode.Type)) + else if (RuleConstant.ACTION_NODE_TYPES.Contains(nextNode.Type)) { // Execute action node - var actionResult = await ExecuteAction(neighborNode, graph, agent, trigger, context); + var actionResult = await ExecuteAction(nextNode, graph, agent, trigger, context); if (actionResult == null) { results.Add(RuleFlowStepResult.FromResult(new() { Success = false, - ErrorMessage = $"Unable to find action {neighborNode.Name}." - }, neighborNode)); + ErrorMessage = $"Unable to find action {nextNode.Name}." + }, nextNode)); continue; } - results.Add(RuleFlowStepResult.FromResult(actionResult, neighborNode)); + results.Add(RuleFlowStepResult.FromResult(actionResult, nextNode)); if (actionResult.IsDelayed) { continue; } - await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNode(nextNode, graph, agent, trigger, text, states, options, results); } else { results.Add(RuleFlowStepResult.FromResult(new() { Success = true, - Response = $"Pass through node {neighborNode.Name}." - }, neighborNode)); - await ExecuteGraphNode(neighborNode, graph, agent, trigger, text, states, options, results); + Response = $"Pass through node {nextNode.Name}." + }, nextNode)); + await ExecuteGraphNode(nextNode, graph, agent, trigger, text, states, options, results); } } } @@ -265,7 +272,7 @@ private async Task ExecuteGraphNode( var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleActionExecuted(agent, node, trigger, context); + await hook.BeforeRuleActionExecuting(agent, node, trigger, context); } // Execute action @@ -351,7 +358,7 @@ private async Task ExecuteGraphNode( var hooks = _services.GetHooks(agent.Id); foreach (var hook in hooks) { - await hook.BeforeRuleConditionExecuted(agent, node, trigger, context); + await hook.BeforeRuleConditionExecuting(agent, node, trigger, context); } // Execute condition diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index c00919db2..91fbee76d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -1,4 +1,6 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Rules; +using BotSharp.Abstraction.Rules.Models; namespace BotSharp.OpenAPI.Controllers; @@ -18,15 +20,15 @@ public IEnumerable GetRuleTriggers() } [HttpGet("/rule/config/options")] - public async Task> GetRuleConfigOptions() + public async Task> GetRuleConfigOptions() { - var dict = new Dictionary(); - var configs = _services.GetServices(); + var dict = new Dictionary(); + var flows = _services.GetServices>(); - foreach (var config in configs) + foreach (var flow in flows) { - var json = await config.GetConfigAsync(); - dict[config.Provider.ToLower()] = json; + var config = await flow.GetTopologyConfigAsync(); + dict[config.TopologyProvider.ToLower()] = config; } return dict; diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index 756e282b3..e04154eda 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -40,7 +40,6 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) #if DEBUG services.AddScoped, DemoRuleGraph>(); - services.AddScoped(); #endif } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs deleted file mode 100644 index 641f30f3b..000000000 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -using BotSharp.Abstraction.Rules; -using System.Text.Json; - -namespace BotSharp.Plugin.Membase.Services; - -public class DemoRuleConfig : IRuleConfig -{ - private readonly IServiceProvider _services; - - public DemoRuleConfig( - IServiceProvider services) - { - _services = services; - } - - public string Provider => "membase"; - - public async Task GetConfigAsync() - { - var settings = _services.GetRequiredService(); - var apiKey = settings.ApiKey; - var projectId = "68503047c5796a8049634a51"; - var graphId = "69a76a0ea77b9871345de795"; - var query = "MATCH%20(a)-[r]-%3E(b)%20WITH%20a,%20r,%20b%20WHERE%20a.agent%20=%20$agent%20AND%20a.trigger%20=%20$trigger%20AND%20b.agent%20=%20$agent%20AND%20b.trigger%20=%20$trigger%20RETURN%20a,%20r,%20b%20LIMIT%20100"; - - return JsonDocument.Parse(JsonSerializer.Serialize(new - { - source = "membase", - htmlTag = "iframe", - url = $"https://console.membase.dev/query-editor/{projectId}?graphId={graphId}&query={query}&token={apiKey}" - })); - } -} diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 975caeaea..6bb92caaf 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -2,6 +2,7 @@ using BotSharp.Abstraction.Graph; using BotSharp.Abstraction.Graph.Models; using BotSharp.Abstraction.Rules; +using BotSharp.Abstraction.Rules.Models; using BotSharp.Abstraction.Rules.Options; using BotSharp.Abstraction.Utilities; using Microsoft.Extensions.Logging; @@ -24,6 +25,27 @@ public DemoRuleGraph( public string Provider => "demo"; + public async Task GetTopologyConfigAsync() + { + var settings = _services.GetRequiredService(); + var apiKey = settings.ApiKey; + var projectId = settings.ProjectId; + var graphId = settings.GraphInstances?.FirstOrDefault(x => x.Purpose.IsEqualTo("rule"))?.Id ?? string.Empty; + var query = Uri.EscapeDataString("MATCH (a)-[r]->(b) WITH a, r, b WHERE a.agent = $agent AND a.trigger = $trigger AND b.agent = $agent AND b.trigger = $trigger RETURN a, r, b LIMIT 100"); + + return new RuleConfigModel + { + TopologyProvider = Provider, + TopologyId = graphId, + CustomConfig = JsonDocument.Parse(JsonSerializer.Serialize(new + { + htmlTag = "iframe", + appendParameterName = "parameters", + url = $"https://console.membase.dev/query-editor/{projectId}?graphId={graphId}&query={query}&token={apiKey}" + })) + }; + } + public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null) { if (string.IsNullOrEmpty(id)) @@ -31,13 +53,17 @@ public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? op return GetDefaultGraph(); } - var query = $""" - MATCH (a)-[r]->(b) - WITH a, r, b - WHERE a.agent = $agent AND a.trigger = $trigger AND b.agent = $agent AND b.trigger = $trigger - RETURN a, r, b - LIMIT 100 - """; + var query = options?.Query ?? string.Empty; + if (string.IsNullOrEmpty(query)) + { + query = $""" + MATCH (a)-[r]->(b) + WITH a, r, b + WHERE a.agent = $agent AND a.trigger = $trigger AND b.agent = $agent AND b.trigger = $trigger + RETURN a, r, b + LIMIT 100 + """; + } var args = new Dictionary(); if (options?.Parameters != null) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs b/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs index 77bf58351..dde67a84a 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs @@ -5,4 +5,23 @@ public class MembaseSettings public string Host { get; set; } = "localhost"; public string ProjectId { get; set; } = string.Empty; public string ApiKey { get; set; } = string.Empty; + public GraphInstance[] GraphInstances { get; set; } = []; } + +public class GraphInstance +{ + /// + /// Graph id + /// + public string Id { get; set; } + + /// + /// Graph name + /// + public string Name { get; set; } + + /// + /// Graph purpose, i.e., rule, etc. + /// + public string Purpose { get; set; } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index ff8442c01..bbcb402cf 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -33,8 +33,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) [BsonIgnoreExtraElements(Inherited = true)] public class RuleConfigMongoModel { - public string? Topology { get; set; } - public string? Provider { get; set; } + public string? TopologyProvider { get; set; } public static RuleConfigMongoModel? ToMongoModel(RuleConfig? config) { @@ -45,8 +44,7 @@ public class RuleConfigMongoModel return new RuleConfigMongoModel { - Topology = config.Topology, - Provider = config.Provider + TopologyProvider = config.TopologyProvider }; } @@ -59,8 +57,7 @@ public class RuleConfigMongoModel return new RuleConfig { - Topology = config.Topology, - Provider = config.Provider + TopologyProvider = config.TopologyProvider }; } } From 819c1df37652a7002ee98d7de6a53b2594a75c33 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 6 Mar 2026 14:38:18 -0600 Subject: [PATCH 75/91] refine config --- .../BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs | 2 +- .../BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs | 4 ++-- src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs index f105a66d3..43ac308b6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs @@ -6,5 +6,5 @@ public class RuleConfigModel { public string TopologyId { get; set; } public string TopologyProvider { get; set; } - public JsonDocument CustomConfig { get; set; } + public JsonDocument CustomParameters { get; set; } = JsonDocument.Parse("{}"); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index 4da1c8335..700705b2e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -5,12 +5,12 @@ public class RuleFlowOptions /// /// Flow topology provider /// - public string TopologyProvider { get; set; } = string.Empty; + public string? TopologyProvider { get; set; } /// /// Flow topology id /// - public string TopologyId { get; set; } = string.Empty; + public string? TopologyId { get; set; } /// /// Query to get flow topology diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 6bb92caaf..18d972c85 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -37,7 +37,7 @@ public async Task GetTopologyConfigAsync() { TopologyProvider = Provider, TopologyId = graphId, - CustomConfig = JsonDocument.Parse(JsonSerializer.Serialize(new + CustomParameters = JsonDocument.Parse(JsonSerializer.Serialize(new { htmlTag = "iframe", appendParameterName = "parameters", From ad46221e7d92e068d9e6a058f6176b6da802d241 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Fri, 6 Mar 2026 18:45:36 -0600 Subject: [PATCH 76/91] add graph clear --- .../BotSharp.Abstraction/Agents/Models/RuleGraph.cs | 6 ++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 9364b5dab..567de441a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -127,6 +127,12 @@ public RuleGraphInfo GetGraphInfo() }; } + public void Clear() + { + _nodes = []; + _edges = []; + } + public static RuleGraph FromGraphInfo(RuleGraphInfo graphInfo) { var graph = new RuleGraph(); diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index bf8ae7b58..955ddd5e5 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -56,12 +56,14 @@ public async Task> Triggered(IRuleTrigger trigger, string te var root = graph.GetRootNode(rootNodeName); if (root == null) { + graph.Clear(); continue; } // 3. Execute graph var execResults = new List(); await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); + graph.Clear(); // Get conversation id to support legacy features var convIds = execResults.Where(x => x.Success && x.Data.TryGetValue("conversation_id", out _)) @@ -105,6 +107,7 @@ await ExecuteGraphNode( options.States, triggerOptions, execResults); + graph.Clear(); } #region Graph From 0f3172d238ba8c4c5d6ce5be18df2a77ad56b4d2 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Sat, 7 Mar 2026 12:03:41 -0600 Subject: [PATCH 77/91] add bfs and refine rule graph --- .../Agents/Models/RuleGraph.cs | 25 +++- .../Rules/Options/RuleFlowOptions.cs | 5 + .../Constants/RuleConstant.cs | 9 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 141 +++++++++++++++++- 4 files changed, 162 insertions(+), 18 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index 567de441a..e8fa9f39f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -28,9 +28,22 @@ public static RuleGraph Init() return _nodes.FirstOrDefault(x => x.Type.IsEqualTo("root") || x.Type.IsEqualTo("start")); } - public RuleNode? GetNode(string id) + public (RuleNode? Node, IEnumerable IncomingEdges, IEnumerable OutgoingEdges) GetNode(string id) { - return _nodes.FirstOrDefault(x => x.Id.IsEqualTo(id)); + var node = _nodes.FirstOrDefault(x => x.Id.IsEqualTo(id)); + if (node == null) + { + return (null, [], []); + } + + var incomingEdges = _edges + .Where(e => e.To != null && e.To.Id.IsEqualTo(id)) + .ToList(); + var outgoingEdges = _edges + .Where(e => e.From != null && e.From.Id.IsEqualTo(id)) + .ToList(); + + return (node, incomingEdges, outgoingEdges); } public string GetGraphId() @@ -38,14 +51,14 @@ public string GetGraphId() return _id; } - public IEnumerable GetNodes() + public IEnumerable GetNodes(Func? filter = null) { - return [.. _nodes]; + return filter == null ? [.. _nodes] : [.. _nodes.Where(filter)]; } - public IEnumerable GetEdges() + public IEnumerable GetEdges(Func? filter = null) { - return [.. _edges]; + return filter == null ? [.. _edges] : [.. _edges.Where(filter)]; } public void SetGraphId(string id) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index 700705b2e..ba2c8b47d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -17,6 +17,11 @@ public class RuleFlowOptions /// public string? Query { get; set; } + /// + /// Graph traversal algorithm: "dfs" (default) or "bfs" + /// + public string TraversalAlgorithm { get; set; } = "dfs"; + /// /// Additional custom parameters, e.g., root_node_name, max_recursion /// diff --git a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs index aed9fd2f2..9598df345 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Constants/RuleConstant.cs @@ -2,7 +2,7 @@ namespace BotSharp.Core.Rules.Constants; public static class RuleConstant { - public const int MAX_GRAPH_RECURSION = 30; + public const int MAX_GRAPH_RECURSION = 50; public static IEnumerable CONDITION_NODE_TYPES = new List { @@ -14,11 +14,4 @@ public static class RuleConstant { "action" }; - - public static IEnumerable END_NODE_TYPES = new List - { - "root", - "start", - "end" - }; } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 955ddd5e5..b48655fbb 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -149,6 +149,26 @@ private async Task ExecuteGraphNode( IEnumerable? states, RuleTriggerOptions? options, List results) + { + if (options?.Flow?.TraversalAlgorithm?.IsEqualTo("bfs") == true) + { + await ExecuteGraphNodeBfs(node, graph, agent, trigger, text, states, options, results); + } + else + { + await ExecuteGraphNodeDfs(node, graph, agent, trigger, text, states, options, results); + } + } + + private async Task ExecuteGraphNodeDfs( + RuleNode node, + RuleGraph graph, + Agent agent, + IRuleTrigger trigger, + string text, + IEnumerable? states, + RuleTriggerOptions? options, + List results) { // Check whether the action nodes have been visited more than limit var visited = results.Count(); @@ -185,7 +205,6 @@ private async Task ExecuteGraphNode( JsonOptions = options?.JsonOptions }; - if (RuleConstant.CONDITION_NODE_TYPES.Contains(nextNode.Type)) { // Execute condition node @@ -205,7 +224,7 @@ private async Task ExecuteGraphNode( // If condition result is true, then execute the next node, otherwise skip if (conditionResult.Success) { - await ExecuteGraphNode(nextNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, options, results); } else { @@ -234,7 +253,7 @@ private async Task ExecuteGraphNode( continue; } - await ExecuteGraphNode(nextNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, options, results); } else { @@ -243,7 +262,121 @@ private async Task ExecuteGraphNode( Success = true, Response = $"Pass through node {nextNode.Name}." }, nextNode)); - await ExecuteGraphNode(nextNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, options, results); + } + } + } + + private async Task ExecuteGraphNodeBfs( + RuleNode root, + RuleGraph graph, + Agent agent, + IRuleTrigger trigger, + string text, + IEnumerable? states, + RuleTriggerOptions? options, + List results) + { + var param = options?.Flow?.Parameters ?? []; + var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 + ? depth : RuleConstant.MAX_GRAPH_RECURSION; + + // Each queue entry is (node-to-process, edge-that-leads-to-it) + var queue = new Queue<(RuleNode Node, RuleEdge Edge)>(); + + foreach (var (childNode, edge) in graph.GetChildrenNodes(root)) + { + queue.Enqueue((childNode, edge)); + } + + while (queue.Count > 0) + { + if (results.Count >= maxRecursion) + { + _logger.LogWarning("Exceed max graph nodes {MaxNodes} during BFS (agent {Agent} and trigger {Trigger}).", + maxRecursion, agent.Name, trigger.Name); + break; + } + + var (nextNode, nextEdge) = queue.Dequeue(); + + var context = new RuleFlowContext + { + Node = nextNode, + Edge = nextEdge, + Graph = graph, + Text = text, + Parameters = BuildParameters(nextNode.Config, states), + PrevStepResults = results, + JsonOptions = options?.JsonOptions + }; + + if (RuleConstant.CONDITION_NODE_TYPES.Contains(nextNode.Type)) + { + // Execute condition node + var conditionResult = await ExecuteCondition(nextNode, graph, agent, trigger, context); + if (conditionResult == null) + { + results.Add(RuleFlowStepResult.FromResult(new() + { + Success = false, + ErrorMessage = $"Unable to find condition {nextNode.Name}." + }, nextNode)); + continue; + } + + results.Add(RuleFlowStepResult.FromResult(conditionResult, nextNode)); + + // If condition is true, enqueue children; otherwise skip the branch + if (conditionResult.Success) + { + foreach (var (childNode, childEdge) in graph.GetChildrenNodes(nextNode)) + { + queue.Enqueue((childNode, childEdge)); + } + } + else + { + _logger.LogInformation("Condition {ConditionName} evaluated to false, skipping next node (agent {Agent} and trigger {Trigger}).", + nextNode.Name, agent.Name, trigger.Name); + } + } + else if (RuleConstant.ACTION_NODE_TYPES.Contains(nextNode.Type)) + { + // Execute action node + var actionResult = await ExecuteAction(nextNode, graph, agent, trigger, context); + if (actionResult == null) + { + results.Add(RuleFlowStepResult.FromResult(new() + { + Success = false, + ErrorMessage = $"Unable to find action {nextNode.Name}." + }, nextNode)); + continue; + } + + results.Add(RuleFlowStepResult.FromResult(actionResult, nextNode)); + + if (!actionResult.IsDelayed) + { + foreach (var (childNode, childEdge) in graph.GetChildrenNodes(nextNode)) + { + queue.Enqueue((childNode, childEdge)); + } + } + } + else + { + results.Add(RuleFlowStepResult.FromResult(new() + { + Success = true, + Response = $"Pass through node {nextNode.Name}." + }, nextNode)); + + foreach (var (childNode, childEdge) in graph.GetChildrenNodes(nextNode)) + { + queue.Enqueue((childNode, childEdge)); + } } } } From da9be1b6a1460b5afc44662f59d52d9f1af6d9c9 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 9 Mar 2026 11:35:41 -0500 Subject: [PATCH 78/91] refine get config --- .../BotSharp.Abstraction/Rules/IRuleFlow.cs | 3 ++- .../Rules/Options/RuleFlowConfigOptions.cs | 7 +++++++ .../Services/DemoRuleGraph.cs | 17 +++++++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs index 37b0126cb..65cdaf446 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs @@ -12,8 +12,9 @@ public interface IRuleFlow where T : class /// /// Get rule flow topology config /// + /// /// - Task GetTopologyConfigAsync(); + Task GetTopologyConfigAsync(RuleFlowConfigOptions? options = null); /// /// Get rule flow topology diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs new file mode 100644 index 000000000..a424856eb --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Rules.Options; + +public class RuleFlowConfigOptions +{ + public string? TopologyId { get; set; } + public string? Purpose { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 18d972c85..8bdb3b331 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -25,12 +25,25 @@ public DemoRuleGraph( public string Provider => "demo"; - public async Task GetTopologyConfigAsync() + public async Task GetTopologyConfigAsync(RuleFlowConfigOptions? options = null) { var settings = _services.GetRequiredService(); var apiKey = settings.ApiKey; var projectId = settings.ProjectId; - var graphId = settings.GraphInstances?.FirstOrDefault(x => x.Purpose.IsEqualTo("rule"))?.Id ?? string.Empty; + + var foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Id.IsEqualTo(options?.TopologyId)); + if (foundInstance == null && !string.IsNullOrEmpty(options?.Purpose)) + { + foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Purpose.IsEqualTo(options.Purpose)); + } + + if (foundInstance == null) + { + // default + foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Purpose.IsEqualTo("rule")); + } + + var graphId = foundInstance?.Id ?? string.Empty; var query = Uri.EscapeDataString("MATCH (a)-[r]->(b) WITH a, r, b WHERE a.agent = $agent AND a.trigger = $trigger AND b.agent = $agent AND b.trigger = $trigger RETURN a, r, b LIMIT 100"); return new RuleConfigModel From ef61dcdfbd116cfd9199ef994f4fdfc38907dac1 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 9 Mar 2026 14:33:17 -0500 Subject: [PATCH 79/91] return all states --- .../BotSharp.Core.Rules/Actions/ChatRuleAction.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs index 257b874f0..e525ad9a0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatRuleAction.cs @@ -53,19 +53,20 @@ await convService.SendMessage(agent.Id, null, msg => Task.CompletedTask); + var data = new Dictionary(convService.States.GetStates() ?? []); await convService.SaveStates(); _logger.LogInformation("Chat rule action executed successfully for agent {AgentId}, conversation {ConversationId}", agent.Id, conv.Id); + + data["agent_id"] = agent.Id; + data["conversation_id"] = conv.Id; + return new RuleNodeResult { Success = true, Response = conv.Id, - Data = new() - { - ["agent_id"] = agent.Id, - ["conversation_id"] = conv.Id - } + Data = data }; } catch (Exception ex) From f1cb51dca6e864c375bd29a10502ff660519d6ec Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 9 Mar 2026 18:03:01 -0500 Subject: [PATCH 80/91] temp save --- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index b48655fbb..6492a10f0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -62,7 +62,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te // 3. Execute graph var execResults = new List(); - await ExecuteGraphNode(root, graph, agent, trigger, text, states, options, execResults); + await ExecuteGraphNode(root, graph, agent, trigger, text, states, null, options, execResults); graph.Clear(); // Get conversation id to support legacy features @@ -105,6 +105,7 @@ await ExecuteGraphNode( agent, trigger, options.Text, options.States, + null, triggerOptions, execResults); graph.Clear(); @@ -147,6 +148,7 @@ private async Task ExecuteGraphNode( IRuleTrigger trigger, string text, IEnumerable? states, + Dictionary? data, RuleTriggerOptions? options, List results) { @@ -156,7 +158,7 @@ private async Task ExecuteGraphNode( } else { - await ExecuteGraphNodeDfs(node, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(node, graph, agent, trigger, text, states, data, options, results); } } @@ -167,6 +169,7 @@ private async Task ExecuteGraphNodeDfs( IRuleTrigger trigger, string text, IEnumerable? states, + Dictionary? data, RuleTriggerOptions? options, List results) { @@ -200,7 +203,7 @@ private async Task ExecuteGraphNodeDfs( Edge = edge, Graph = graph, Text = text, - Parameters = BuildParameters(nextNode.Config, states), + Parameters = BuildParameters(nextNode.Config, states, data), PrevStepResults = results, JsonOptions = options?.JsonOptions }; @@ -224,7 +227,7 @@ private async Task ExecuteGraphNodeDfs( // If condition result is true, then execute the next node, otherwise skip if (conditionResult.Success) { - await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, context.Parameters, options, results); } else { @@ -253,7 +256,7 @@ private async Task ExecuteGraphNodeDfs( continue; } - await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, data, options, results); } else { @@ -262,7 +265,7 @@ private async Task ExecuteGraphNodeDfs( Success = true, Response = $"Pass through node {nextNode.Name}." }, nextNode)); - await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, data, options, results); } } } @@ -556,7 +559,10 @@ private async Task ExecuteGraphNodeBfs( #region Private methods - private Dictionary BuildParameters(Dictionary? config, IEnumerable? states) + private Dictionary BuildParameters( + Dictionary? config, + IEnumerable? states, + Dictionary? param = null) { var dict = new Dictionary(); @@ -573,6 +579,14 @@ private async Task ExecuteGraphNodeBfs( } } + if (!param.IsNullOrEmpty()) + { + foreach (var pair in param!) + { + dict[pair.Key] = pair.Value; + } + } + return dict; } #endregion From 7f8de529d9d2a38db5c254d2efb756d6ccf21427 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Mon, 9 Mar 2026 22:54:35 -0500 Subject: [PATCH 81/91] minor change --- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6492a10f0..850b5e57c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -154,7 +154,7 @@ private async Task ExecuteGraphNode( { if (options?.Flow?.TraversalAlgorithm?.IsEqualTo("bfs") == true) { - await ExecuteGraphNodeBfs(node, graph, agent, trigger, text, states, options, results); + await ExecuteGraphNodeBfs(node, graph, agent, trigger, text, states, data, options, results); } else { @@ -179,6 +179,8 @@ private async Task ExecuteGraphNodeDfs( var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; + var innerData = new Dictionary(data ?? []); + if (visited >= maxRecursion) { _logger.LogWarning("Exceed max graph recursion {MaxRecursion} (agent {Agent} and trigger {Trigger}).", @@ -203,7 +205,7 @@ private async Task ExecuteGraphNodeDfs( Edge = edge, Graph = graph, Text = text, - Parameters = BuildParameters(nextNode.Config, states, data), + Parameters = BuildParameters(nextNode.Config, states, innerData), PrevStepResults = results, JsonOptions = options?.JsonOptions }; @@ -277,6 +279,7 @@ private async Task ExecuteGraphNodeBfs( IRuleTrigger trigger, string text, IEnumerable? states, + Dictionary? data, RuleTriggerOptions? options, List results) { @@ -284,6 +287,8 @@ private async Task ExecuteGraphNodeBfs( var maxRecursion = int.TryParse(param.GetValueOrDefault("max_recursion")?.ToString(), out var depth) && depth > 0 ? depth : RuleConstant.MAX_GRAPH_RECURSION; + var innerData = new Dictionary(data ?? []); + // Each queue entry is (node-to-process, edge-that-leads-to-it) var queue = new Queue<(RuleNode Node, RuleEdge Edge)>(); @@ -309,7 +314,7 @@ private async Task ExecuteGraphNodeBfs( Edge = nextEdge, Graph = graph, Text = text, - Parameters = BuildParameters(nextNode.Config, states), + Parameters = BuildParameters(nextNode.Config, states, innerData), PrevStepResults = results, JsonOptions = options?.JsonOptions }; @@ -318,6 +323,8 @@ private async Task ExecuteGraphNodeBfs( { // Execute condition node var conditionResult = await ExecuteCondition(nextNode, graph, agent, trigger, context); + innerData = new(context.Parameters ?? []); + if (conditionResult == null) { results.Add(RuleFlowStepResult.FromResult(new() @@ -348,6 +355,8 @@ private async Task ExecuteGraphNodeBfs( { // Execute action node var actionResult = await ExecuteAction(nextNode, graph, agent, trigger, context); + innerData = new(context.Parameters ?? []); + if (actionResult == null) { results.Add(RuleFlowStepResult.FromResult(new() From 36540a0e37963b8465d65a775121350422d9cc2c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 10 Mar 2026 14:57:20 -0500 Subject: [PATCH 82/91] refine rule engine --- .../Rules/Hooks/IRuleTriggerHook.cs | 4 +- .../BotSharp.Abstraction/Rules/IRuleFlow.cs | 2 +- .../Conditions/AllVisitedRuleCondition.cs | 32 ++++ .../Conditions/ExampleRuleCondition.cs | 68 ------- .../Conditions/LoopingCondition.cs | 167 ++++++++++++++++++ .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 8 +- .../BotSharp.Core.Rules/RulesPlugin.cs | 4 +- .../Controllers/MembaseController.cs | 48 +++-- .../Models/Requests/EdgeUpdateModel.cs | 1 + .../Services/DemoRuleGraph.cs | 2 +- 10 files changed, 248 insertions(+), 88 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs delete mode 100644 src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs index c2ed36bb4..49f2029a7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Hooks/IRuleTriggerHook.cs @@ -6,8 +6,8 @@ namespace BotSharp.Abstraction.Rules.Hooks; public interface IRuleTriggerHook : IHookBase { Task BeforeRuleConditionExecuting(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; - Task AfterRuleConditionExecuted(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleNodeResult result) => Task.CompletedTask; + Task AfterRuleConditionExecuted(Agent agent, RuleNode conditionNode, IRuleTrigger trigger, RuleFlowContext context, RuleNodeResult result) => Task.CompletedTask; Task BeforeRuleActionExecuting(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; - Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleNodeResult result) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, IRuleTrigger trigger, RuleFlowContext context, RuleNodeResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs index 65cdaf446..26ac106a4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs @@ -22,5 +22,5 @@ public interface IRuleFlow where T : class /// /// /// - Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null); + Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs new file mode 100644 index 000000000..c9210c690 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs @@ -0,0 +1,32 @@ +namespace BotSharp.Core.Rules.Conditions; + +public class AllVisitedRuleCondition : IRuleCondition +{ + private readonly ILogger _logger; + + public AllVisitedRuleCondition( + ILogger logger) + { + _logger = logger; + } + + public string Name => "all_visited"; + + public async Task EvaluateAsync( + Agent agent, + IRuleTrigger trigger, + RuleFlowContext context) + { + var currentNode = context.Node; + var parents = context.Graph.GetParentNodes(currentNode); + var parentNodeIds = parents.Select(x => x.Item1.Id).ToList(); + var visitedNodeIds = context.PrevStepResults?.Select(x => x.Node.Id)?.ToHashSet() ?? []; + var allVisited = parentNodeIds.All(x => visitedNodeIds.Contains(x)); + + return new RuleNodeResult + { + Success = allVisited, + Response = allVisited ? "All parent nodes have been visited" : "Missing parenet nodes visiting." + }; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs deleted file mode 100644 index 92410991e..000000000 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/ExampleRuleCondition.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace BotSharp.Core.Rules.Conditions; - -/// -/// Example rule condition that demonstrates how to implement IRuleCondition. -/// This condition checks if a parameter value matches an expected value. -/// -public sealed class ExampleRuleCondition : IRuleCondition -{ - private readonly ILogger _logger; - - public ExampleRuleCondition(ILogger logger) - { - _logger = logger; - } - - public string Name => "example_condition"; - - // Default configuration example: - // { - // "parameter_name": "status", - // "expected_value": "active" - // } - - public async Task EvaluateAsync( - Agent agent, - IRuleTrigger trigger, - RuleFlowContext context) - { - try - { - var parameterName = context.Parameters.GetValueOrDefault("parameter_name", "status"); - var expectedValue = context.Parameters.GetValueOrDefault("expected_value", "active"); - var actualValue = context.Parameters.GetValueOrDefault(parameterName, string.Empty); - - _logger.LogInformation("Evaluating condition: {ParameterName} = {ActualValue}, expected = {ExpectedValue}", - parameterName, actualValue, expectedValue); - - var isMatch = actualValue?.Equals(expectedValue, StringComparison.OrdinalIgnoreCase) == true; - - if (isMatch) - { - return new RuleNodeResult - { - Success = isMatch, - Response = $"Condition met: {parameterName} = {actualValue}" - }; - } - else - { - return new RuleNodeResult - { - Success = isMatch, - Response = $"Condition not met: {parameterName} = {actualValue}, expected = {expectedValue}" - }; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error evaluating example condition for agent {AgentId}", agent.Id); - return new RuleNodeResult - { - Success = false, - ErrorMessage = ex.Message - }; - } - } -} - diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs new file mode 100644 index 000000000..91f674330 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs @@ -0,0 +1,167 @@ +using System.Text.Json; + +namespace BotSharp.Core.Rules.Conditions; + +/// +/// A general loop condition node that iterates over a list of items in context parameters. +/// +/// Expected parameters: +/// - "list_items": A JSON array of items to iterate over (e.g. ["a","b","c"] or [1,2,3] or [{...},{...}]). +/// - "iterate_index": The current iteration index (auto-managed, starts at 0). +/// - "iterate_current_item": The current item being processed (auto-set each iteration). +/// +/// Flow: +/// Action Node → LoopCondition → (true) → back to Action Node +/// → (false) → resets list_items, iterate_index, iterate_current_item and continues +/// +public sealed class LoopingCondition : IRuleCondition +{ + private const string PARAM_LIST_ITEMS = "list_items"; + private const string PARAM_LIST_ITEMS_KEY = "list_items_key"; + private const string PARAM_ITERATE_INDEX = "iterate_index"; + private const string PARAM_ITERATE_ITEM_KEY = "iterate_item_key"; + private const string PARAM_ITERATE_NEXT_ITEM = "iterate_next_item"; + + private readonly ILogger _logger; + + public LoopingCondition(ILogger logger) + { + _logger = logger; + } + + public string Name => "looping"; + + public async Task EvaluateAsync( + Agent agent, + IRuleTrigger trigger, + RuleFlowContext context) + { + try + { + context.Parameters ??= []; + + var listItemsRaw = string.Empty; + var listItemsKey = context.Parameters.GetValueOrDefault(PARAM_LIST_ITEMS_KEY, string.Empty); + if (!string.IsNullOrWhiteSpace(listItemsKey)) + { + listItemsRaw = context.Parameters.GetValueOrDefault(listItemsKey, string.Empty); + } + else + { + listItemsRaw = context.Parameters.GetValueOrDefault(PARAM_LIST_ITEMS, string.Empty); + } + + if (string.IsNullOrWhiteSpace(listItemsRaw)) + { + _logger.LogInformation("Loop condition: list items are empty, loop completed (agent {AgentId}).", agent.Id); + CleanLoopState(context); + return new RuleNodeResult + { + Success = false, + Response = "Loop completed: list_items is empty." + }; + } + + // Deserialize list_items as a JSON array of any type + var items = JsonSerializer.Deserialize(listItemsRaw, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (items.IsNullOrEmpty()) + { + _logger.LogInformation("Loop condition: no items to iterate, loop completed (agent {AgentId}).", agent.Id); + CleanLoopState(context); + return new RuleNodeResult + { + Success = false, + Response = "Loop completed: no items in list." + }; + } + + // If iterate_index is not yet set, this is the first visit after the action node + // already handled item[0], so start from index 1. + var indexStr = context.Parameters.GetValueOrDefault(PARAM_ITERATE_INDEX); + int currentIndex; + if (string.IsNullOrEmpty(indexStr)) + { + currentIndex = 0; + } + else if (!int.TryParse(indexStr, out currentIndex)) + { + currentIndex = 0; + } + + var nextIndex = currentIndex + 1; + if (currentIndex >= items!.Length || nextIndex >= items!.Length) + { + _logger.LogInformation("Loop condition: iteration complete ({Index}/{Total}) (agent {AgentId}).", + currentIndex, items.Length, agent.Id); + CleanLoopState(context); + return new RuleNodeResult + { + Success = false, + Response = $"Loop completed: iterated over all {items.Length} items." + }; + } + + // Set next item and advance index + var nextElement = items[nextIndex]; + var nextItem = nextElement.ConvertToString(); + context.Parameters[PARAM_ITERATE_NEXT_ITEM] = nextItem; + context.Parameters[PARAM_ITERATE_INDEX] = nextIndex.ToString(); + + var data = new Dictionary + { + [PARAM_ITERATE_NEXT_ITEM] = nextItem, + [PARAM_ITERATE_INDEX] = nextIndex.ToString() + }; + + + var itemKey = context.Parameters.GetValueOrDefault(PARAM_ITERATE_ITEM_KEY); + if (!string.IsNullOrEmpty(itemKey) + && nextElement.ValueKind == JsonValueKind.Object + && nextElement.TryGetProperty(itemKey, out var fieldValue)) + { + var fieldStr = fieldValue.ToString(); + context.Parameters[itemKey] = fieldStr; + data[itemKey] = fieldStr; + } + + _logger.LogInformation("Loop condition: processing item {Index}/{Total} = '{Item}' (agent {AgentId}).", + nextItem, items.Length, nextElement, agent.Id); + + return new RuleNodeResult + { + Success = true, + Response = $"Loop iteration {nextIndex}/{items.Length}: next item = {nextItem}", + Data = data + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error evaluating loop condition for agent {AgentId}", agent.Id); + CleanLoopState(context); + return new RuleNodeResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + + private void CleanLoopState(RuleFlowContext context) + { + var itemKey = context.Parameters?.GetValueOrDefault(PARAM_ITERATE_ITEM_KEY); + + context.Parameters?.Remove(PARAM_LIST_ITEMS); + context.Parameters?.Remove(PARAM_ITERATE_INDEX); + context.Parameters?.Remove(PARAM_ITERATE_ITEM_KEY); + context.Parameters?.Remove(PARAM_ITERATE_NEXT_ITEM); + + if (!string.IsNullOrEmpty(itemKey)) + { + context.Parameters?.Remove(itemKey); + } + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 850b5e57c..5957753ef 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -258,7 +258,7 @@ private async Task ExecuteGraphNodeDfs( continue; } - await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, data, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, context.Parameters, options, results); } else { @@ -267,7 +267,7 @@ private async Task ExecuteGraphNodeDfs( Success = true, Response = $"Pass through node {nextNode.Name}." }, nextNode)); - await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, data, options, results); + await ExecuteGraphNodeDfs(nextNode, graph, agent, trigger, text, states, context.Parameters, options, results); } } } @@ -429,7 +429,7 @@ private async Task ExecuteGraphNodeBfs( foreach (var hook in hooks) { - await hook.AfterRuleActionExecuted(agent, node, trigger, result); + await hook.AfterRuleActionExecuted(agent, node, trigger, context, result); } return result; @@ -515,7 +515,7 @@ private async Task ExecuteGraphNodeBfs( foreach (var hook in hooks) { - await hook.AfterRuleConditionExecuted(agent, node, trigger, result); + await hook.AfterRuleConditionExecuted(agent, node, trigger, context, result); } return result; diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 569bc4555..25f09dbd4 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Crontab.Settings; using BotSharp.Abstraction.Rules.Settings; using BotSharp.Core.Rules.Actions; using BotSharp.Core.Rules.Conditions; @@ -35,7 +34,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) #if DEBUG // Register rule conditions - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Register rule trigger services.AddScoped(); diff --git a/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs b/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs index 8ab4ff3e8..52f3968dd 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Controllers/MembaseController.cs @@ -24,6 +24,9 @@ public MembaseController( /// /// The graph identifier /// Graph information +#if DEBUG + [AllowAnonymous] +#endif [HttpGet("/membase/{graphId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -54,6 +57,9 @@ public async Task GetGraphInfo(string graphId) /// The graph identifier /// The Cypher query request containing the query and parameters /// Query result with columns and data +#if DEBUG + [AllowAnonymous] +#endif [HttpPost("/membase/{graphId}/query")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -98,6 +104,9 @@ public async Task ExecuteGraphQuery(string graphId, [FromBody] Cy /// The graph identifier /// The node identifier /// The node +#if DEBUG + [AllowAnonymous] +#endif [HttpGet("/membase/{graphId}/node/{nodeId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -133,6 +142,9 @@ public async Task GetNode(string graphId, string nodeId) /// The graph identifier /// The node creation model /// The created node +#if DEBUG + [AllowAnonymous] +#endif [HttpPost("/membase/{graphId}/node")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -166,28 +178,30 @@ public async Task CreateNode(string graphId, [FromBody] NodeCreat /// Merge a node in the graph /// /// The graph identifier - /// The node identifier /// The node update model /// The merged node - [HttpPut("/membase/{graphId}/node/{nodeId}/merge")] +#if DEBUG + [AllowAnonymous] +#endif + [HttpPut("/membase/{graphId}/node/merge")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task MergeNode(string graphId, string nodeId, [FromBody] NodeUpdateModel request) + public async Task MergeNode(string graphId, [FromBody] NodeUpdateModel request) { if (string.IsNullOrWhiteSpace(graphId)) { return BadRequest("Graph ID cannot be empty."); } - if (string.IsNullOrWhiteSpace(nodeId)) + if (string.IsNullOrWhiteSpace(request?.Id)) { return BadRequest("Node ID cannot be empty."); } try { - var node = await _membaseApi.MergeNodeAsync(graphId, nodeId, request); + var node = await _membaseApi.MergeNodeAsync(graphId, request.Id, request); return Ok(node); } catch (Exception ex) @@ -204,6 +218,9 @@ public async Task MergeNode(string graphId, string nodeId, [FromB /// The graph identifier /// The node identifier /// Delete response +#if DEBUG + [AllowAnonymous] +#endif [HttpDelete("/membase/{graphId}/node/{nodeId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -239,6 +256,9 @@ public async Task DeleteNode(string graphId, string nodeId) /// The graph identifier /// The edge identifier /// The edge +#if DEBUG + [AllowAnonymous] +#endif [HttpGet("/membase/{graphId}/edge/{edgeId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -274,6 +294,9 @@ public async Task GetEdge(string graphId, string edgeId) /// The graph identifier /// The edge creation model /// The created edge +#if DEBUG + [AllowAnonymous] +#endif [HttpPost("/membase/{graphId}/edge")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -322,28 +345,30 @@ public async Task CreateEdge(string graphId, [FromBody] EdgeCreat /// Update an edge in the graph /// /// The graph identifier - /// The edge identifier /// The edge update model /// The updated edge - [HttpPut("/membase/{graphId}/edge/{edgeId}")] +#if DEBUG + [AllowAnonymous] +#endif + [HttpPut("/membase/{graphId}/edge")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task UpdateEdge(string graphId, string edgeId, [FromBody] EdgeUpdateModel request) + public async Task UpdateEdge(string graphId, [FromBody] EdgeUpdateModel request) { if (string.IsNullOrWhiteSpace(graphId)) { return BadRequest("Graph ID cannot be empty."); } - if (string.IsNullOrWhiteSpace(edgeId)) + if (string.IsNullOrWhiteSpace(request?.Id)) { return BadRequest("Edge ID cannot be empty."); } try { - var edge = await _membaseApi.UpdateEdgeAsync(graphId, edgeId, request); + var edge = await _membaseApi.UpdateEdgeAsync(graphId, request.Id, request); return Ok(edge); } catch (Exception ex) @@ -360,6 +385,9 @@ public async Task UpdateEdge(string graphId, string edgeId, [From /// The graph identifier /// The edge identifier /// Delete response +#if DEBUG + [AllowAnonymous] +#endif [HttpDelete("/membase/{graphId}/edge/{edgeId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] diff --git a/src/Plugins/BotSharp.Plugin.Membase/Models/Requests/EdgeUpdateModel.cs b/src/Plugins/BotSharp.Plugin.Membase/Models/Requests/EdgeUpdateModel.cs index 9cc80123f..c660f485d 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Models/Requests/EdgeUpdateModel.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Models/Requests/EdgeUpdateModel.cs @@ -2,5 +2,6 @@ namespace BotSharp.Plugin.Membase.Models; public class EdgeUpdateModel { + public string? Id { get; set; } public object? Properties { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 8bdb3b331..530ae6039 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -59,7 +59,7 @@ public async Task GetTopologyConfigAsync(RuleFlowConfigOptions? }; } - public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null) + public async Task GetTopologyAsync(string id, RuleFlowLoadOptions? options = null) { if (string.IsNullOrEmpty(id)) { From 707167f8bb6b5bd81c19c07ca1d3351f8a30855a Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 10 Mar 2026 15:24:25 -0500 Subject: [PATCH 83/91] minor change --- .../BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index ba2c8b47d..abd62de47 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -5,25 +5,30 @@ public class RuleFlowOptions /// /// Flow topology provider /// + [JsonPropertyName("topology_provider")] public string? TopologyProvider { get; set; } /// /// Flow topology id /// + [JsonPropertyName("topology_id")] public string? TopologyId { get; set; } /// /// Query to get flow topology /// + [JsonPropertyName("query")] public string? Query { get; set; } /// /// Graph traversal algorithm: "dfs" (default) or "bfs" /// + [JsonPropertyName("traversal_algorithm")] public string TraversalAlgorithm { get; set; } = "dfs"; /// /// Additional custom parameters, e.g., root_node_name, max_recursion /// + [JsonPropertyName("parameters")] public Dictionary Parameters { get; set; } = []; } From c491704c04235b24fde8ab4008f7a07750d929b1 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 10 Mar 2026 23:32:54 -0500 Subject: [PATCH 84/91] clean code --- .../Services/DemoRuleGraph.cs | 28 ++++++------------- src/WebStarter/appsettings.json | 3 +- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 530ae6039..1e34564f7 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -63,7 +63,11 @@ public async Task GetTopologyConfigAsync(RuleFlowConfigOptions? { if (string.IsNullOrEmpty(id)) { +#if DEBUG return GetDefaultGraph(); +#else + return null; +#endif } var query = options?.Query ?? string.Empty; @@ -248,16 +252,6 @@ private RuleGraph GetDefaultGraph() Type = "end", }; - var delayNode = new RuleNode - { - Name = "delay_message", - Type = "action", - Config = new() - { - ["delay"] = "3 seconds" - } - }; - var node1 = new RuleNode { Name = "http_request", @@ -265,7 +259,7 @@ private RuleGraph GetDefaultGraph() Config = new() { ["http_method"] = "GET", - ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883958" + ["http_url"] = "https://dummy.restapiexample.com/api/v1/employees" } }; @@ -276,7 +270,7 @@ private RuleGraph GetDefaultGraph() Config = new() { ["http_method"] = "GET", - ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883956" + ["http_url"] = "https://dummy.restapiexample.com/api/v1/employee/1" } }; @@ -287,17 +281,11 @@ private RuleGraph GetDefaultGraph() Config = new() { ["http_method"] = "GET", - ["http_url"] = "https://meshstage.lessen.com/reactivewocore/reactivewos/9883954" + ["http_url"] = "https://dummy.restapiexample.com/api/v1/employee/2" } }; - graph.AddEdge(root, delayNode, payload: new() - { - Name = "edge", - Type = "is_next" - }); - - graph.AddEdge(delayNode, node1, payload: new() + graph.AddEdge(root, node1, payload: new() { Name = "edge", Type = "next" diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 0996487f5..c8934b92f 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -1085,8 +1085,7 @@ "BotSharp.Plugin.PythonInterpreter", "BotSharp.Plugin.FuzzySharp", "BotSharp.Plugin.MMPEmbedding", - "BotSharp.Plugin.MultiTenancy", - "BotSharp.Plugin.RabbitMQ" + "BotSharp.Plugin.MultiTenancy" ] }, From c99b8d9ca0bde8b070a71898e0d5c542ff5dbab3 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 10 Mar 2026 23:45:52 -0500 Subject: [PATCH 85/91] fix --- .../BotSharp.Abstraction/Agents/Models/RuleGraph.cs | 10 ++++++---- .../Rules/Settings/RuleSettings.cs | 5 ----- src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs | 6 ------ .../BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs | 10 +++++----- src/WebStarter/appsettings.json | 8 -------- 5 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs index e8fa9f39f..3258c3c97 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs @@ -1,3 +1,5 @@ +using System.Xml.Linq; + namespace BotSharp.Abstraction.Agents.Models; public class RuleGraph @@ -108,10 +110,10 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) Id = payload.Id, Name = payload.Name, Type = payload.Type, - Labels = payload.Labels, + Labels = [.. payload.Labels ?? []], Weight = payload.Weight, Purpose = payload.Purpose, - Config = payload.Config + Config = new(payload.Config ?? []) }); } } @@ -135,8 +137,8 @@ public RuleGraphInfo GetGraphInfo() return new() { GraphId = _id, - Nodes = _nodes, - Edges = _edges + Nodes = [.. _nodes?.ToList() ?? []], + Edges = [.. _edges?.ToList() ?? []] }; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs deleted file mode 100644 index defa0aa84..000000000 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Settings/RuleSettings.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace BotSharp.Abstraction.Rules.Settings; - -public class RuleSettings -{ -} diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index 25f09dbd4..ace6553ae 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Rules.Settings; using BotSharp.Core.Rules.Actions; using BotSharp.Core.Rules.Conditions; using BotSharp.Core.Rules.Engines; @@ -19,11 +18,6 @@ public class RulesPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - // Register rule settings - var settings = new RuleSettings(); - config.Bind("Rule", settings); - services.AddSingleton(settings); - // Register rule engine services.AddScoped(); diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 1e34564f7..70d35a91d 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -171,7 +171,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Labels = sourceNodeLabels, Name = GetGraphItemAttribute(sourceNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(sourceNodeProps, key: "type", defaultValue: "action"), - Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "empty"), + Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "unknown"), Config = GetConfig(sourceNodeProps) }; @@ -182,7 +182,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Labels = targetNodeLabels, Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "action"), - Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "empty"), + Purpose = GetGraphItemAttribute(targetNodeProps, key: "purpose", defaultValue: "unknown"), Config = GetConfig(targetNodeProps) }; @@ -190,9 +190,9 @@ private RuleGraph BuildGraph(GraphQueryResult result) var edgePayload = new GraphItemPayload() { Id = edgeId ?? Guid.NewGuid().ToString(), - Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "edge"), - Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "next"), - Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "empty"), + Name = GetGraphItemAttribute(edgeProps, key: "name", defaultValue: "edge"), + Type = GetGraphItemAttribute(edgeProps, key: "type", defaultValue: "next"), + Purpose = GetGraphItemAttribute(edgeProps, key: "purpose", defaultValue: "unknown"), Weight = edgeWeight, Config = GetConfig(edgeProps) }; diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index c8934b92f..11858f672 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -736,14 +736,6 @@ "Enabled": false }, - "Rules": { - "ConfigOptions": { - "Graph": [ - "membase" - ] - } - }, - "Crontab": { "Watcher": { "Enabled": false From 405ea827e524263152cd1c330b2e6ee3b486ee67 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Tue, 10 Mar 2026 23:52:42 -0500 Subject: [PATCH 86/91] remove rabbit mq reference --- BotSharp.sln | 11 ----------- src/WebStarter/WebStarter.csproj | 1 - 2 files changed, 12 deletions(-) diff --git a/BotSharp.sln b/BotSharp.sln index 6abc5b47b..ad95f29e8 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -157,8 +157,6 @@ 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.RabbitMQ", "src\Plugins\BotSharp.Plugin.RabbitMQ\BotSharp.Plugin.RabbitMQ.csproj", "{8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -671,14 +669,6 @@ 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 - {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 +745,6 @@ 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} - {8E609A1C-0421-5BB5-DEA9-5FDB68F6D1C5} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index be332a38e..9374d95fd 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -42,7 +42,6 @@ - From c83196152b7a41b5ecdd1de9c908ba01cb6b7c47 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 11 Mar 2026 19:23:21 -0500 Subject: [PATCH 87/91] relocate --- .../{Agents/Models => Rules}/RuleGraph.cs | 8 +++++--- .../Controllers/Agent/AgentController.Rule.cs | 1 - src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs | 1 - .../BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/{Agents/Models => Rules}/RuleGraph.cs (96%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs similarity index 96% rename from src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs rename to src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs index 3258c3c97..632a3df2d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs @@ -1,6 +1,4 @@ -using System.Xml.Linq; - -namespace BotSharp.Abstraction.Agents.Models; +namespace BotSharp.Abstraction.Rules; public class RuleGraph { @@ -40,9 +38,11 @@ public static RuleGraph Init() var incomingEdges = _edges .Where(e => e.To != null && e.To.Id.IsEqualTo(id)) + .OrderByDescending(x => x.Weight) .ToList(); var outgoingEdges = _edges .Where(e => e.From != null && e.From.Id.IsEqualTo(id)) + .OrderByDescending(x => x.Weight) .ToList(); return (node, incomingEdges, outgoingEdges); @@ -121,6 +121,7 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) public IEnumerable<(RuleNode, RuleEdge)> GetParentNodes(RuleNode node) { return _edges.Where(e => e.To != null && e.To.Id.IsEqualTo(node.Id)) + .OrderByDescending(e => e.Weight) .Select(e => (e.From, e)) .ToList(); } @@ -128,6 +129,7 @@ public void AddEdge(RuleNode from, RuleNode to, GraphItemPayload payload) public IEnumerable<(RuleNode, RuleEdge)> GetChildrenNodes(RuleNode node) { return _edges.Where(e => e.From != null && e.From.Id.IsEqualTo(node.Id)) + .OrderByDescending(e => e.Weight) .Select(e => (e.To, e)) .ToList(); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 91fbee76d..b85bb2b35 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Rules; using BotSharp.Abstraction.Rules.Models; diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index e04154eda..4018e8275 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Graph; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Rules; diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 70d35a91d..bf7c1ea61 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Graph; using BotSharp.Abstraction.Graph.Models; using BotSharp.Abstraction.Rules; From 4aa0fc0d782071e5cce52956e10a5c456affdf56 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Wed, 11 Mar 2026 19:52:07 -0500 Subject: [PATCH 88/91] set node weight --- .../BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index bf7c1ea61..c101966c3 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -144,6 +144,9 @@ private RuleGraph BuildGraph(GraphQueryResult result) var sourceNodeProps = sourceNodeElement.TryGetProperty("properties", out var sProps) ? sProps : default; + var sourceNodeWeight = sourceNodeElement.TryGetProperty("weight", out var sNodeWeight) && sNodeWeight.ValueKind == JsonValueKind.Number + ? sNodeWeight.GetDouble() + : 1.0; // Parse target node var targetNodeId = targetNodeElement.GetProperty("id").GetString(); @@ -153,6 +156,9 @@ private RuleGraph BuildGraph(GraphQueryResult result) var targetNodeProps = targetNodeElement.TryGetProperty("properties", out var tProps) ? tProps : default; + var targetNodeWeight = targetNodeElement.TryGetProperty("weight", out var tNodeWeight) && tNodeWeight.ValueKind == JsonValueKind.Number + ? tNodeWeight.GetDouble() + : 1.0; // Parse edge var edgeId = edgeElement.GetProperty("id").GetString(); @@ -168,6 +174,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) { Id = sourceNodeId ?? Guid.NewGuid().ToString(), Labels = sourceNodeLabels, + Weight = sourceNodeWeight, Name = GetGraphItemAttribute(sourceNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(sourceNodeProps, key: "type", defaultValue: "action"), Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: "unknown"), @@ -179,6 +186,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) { Id = targetNodeId ?? Guid.NewGuid().ToString(), Labels = targetNodeLabels, + Weight = targetNodeWeight, Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "action"), Purpose = GetGraphItemAttribute(targetNodeProps, key: "purpose", defaultValue: "unknown"), From 3b81c16f898ee3cb9c7ccc15a62399dc2db94d06 Mon Sep 17 00:00:00 2001 From: Jicheng Lu Date: Sun, 15 Mar 2026 20:12:43 -0500 Subject: [PATCH 89/91] refine topology --- .../Agents/Models/AgentRule.cs | 4 +- .../BotSharp.Abstraction/Rules/IRuleFlow.cs | 4 +- .../Rules/Models/RuleConfigModel.cs | 2 +- .../Rules/Options/RuleFlowConfigOptions.cs | 3 +- .../Rules/Options/RuleFlowLoadOptions.cs | 2 - .../Rules/Options/RuleFlowOptions.cs | 6 +- .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 69 +++++++++++-------- .../Controllers/Agent/AgentController.Rule.cs | 2 +- .../Services/DemoRuleGraph.cs | 17 ++--- .../Settings/MembaseSettings.cs | 4 +- .../Models/AgentRuleMongoElement.cs | 6 +- 11 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 6bcdf78ec..3ae15d104 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -15,6 +15,6 @@ public class AgentRule public class RuleConfig { - [JsonPropertyName("topology_provider")] - public string? TopologyProvider { get; set; } + [JsonPropertyName("topology_name")] + public string? TopologyName { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs index 26ac106a4..71dca2ac9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlow.cs @@ -5,9 +5,9 @@ namespace BotSharp.Abstraction.Rules; public interface IRuleFlow where T : class { /// - /// Rule flow provider + /// Rule flow topology name /// - string Provider { get; } + string Name { get; } /// /// Get rule flow topology config diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs index 43ac308b6..69b5915c0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleConfigModel.cs @@ -5,6 +5,6 @@ namespace BotSharp.Abstraction.Rules.Models; public class RuleConfigModel { public string TopologyId { get; set; } - public string TopologyProvider { get; set; } + public string TopologyName { get; set; } public JsonDocument CustomParameters { get; set; } = JsonDocument.Parse("{}"); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs index a424856eb..ddb3e734f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowConfigOptions.cs @@ -2,6 +2,5 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleFlowConfigOptions { - public string? TopologyId { get; set; } - public string? Purpose { get; set; } + public string? TopologyName { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs index 31705e6bb..38c653bcf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowLoadOptions.cs @@ -2,8 +2,6 @@ namespace BotSharp.Abstraction.Rules.Options; public class RuleFlowLoadOptions { - public string? AgentId { get; set; } - public string? TriggerName { get; set; } public string? Query { get; set; } public Dictionary? Parameters { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs index abd62de47..6d8709107 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs @@ -9,10 +9,10 @@ public class RuleFlowOptions public string? TopologyProvider { get; set; } /// - /// Flow topology id + /// Flow topology name /// - [JsonPropertyName("topology_id")] - public string? TopologyId { get; set; } + [JsonPropertyName("topology_name")] + public string? TopologyName { get; set; } /// /// Query to get flow topology diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 5957753ef..8b5d846a9 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -38,13 +38,13 @@ public async Task> Triggered(IRuleTrigger trigger, string te } var ruleConfig = rule.Config; - var ruleFlowProvider = options?.Flow?.TopologyProvider ?? ruleConfig?.TopologyProvider; + var ruleFlowTopologyName = options?.Flow?.TopologyName ?? ruleConfig?.TopologyName; - if (!string.IsNullOrEmpty(ruleFlowProvider)) + if (!string.IsNullOrEmpty(ruleFlowTopologyName)) { // Execute graph // 1. Load graph - var graph = await LoadGraph(ruleFlowProvider, agent, trigger, options?.Flow); + var graph = await LoadGraph(ruleFlowTopologyName, agent, trigger, options?.Flow); if (graph == null) { continue; @@ -112,33 +112,44 @@ await ExecuteGraphNode( } #region Graph - private async Task LoadGraph(string provider, Agent agent, IRuleTrigger trigger, RuleFlowOptions? options) + private async Task LoadGraph(string name, Agent agent, IRuleTrigger trigger, RuleFlowOptions? options) { - var flow = _services.GetServices>().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + var flow = _services.GetServices>().FirstOrDefault(x => x.Name.IsEqualTo(name)); if (flow == null) { return null; } - var param = new Dictionary(options?.Parameters ?? []); - param["agent"] = param.GetValueOrDefault("agent", agent.Name); - param["agent_id"] = param.GetValueOrDefault("agent_id", agent.Id); - param["trigger"] = param.GetValueOrDefault("trigger", trigger.Name); - - var topologyId = options?.TopologyId; - if (string.IsNullOrEmpty(topologyId)) + try { - var config = await flow.GetTopologyConfigAsync(); - topologyId = config.TopologyId; - } + var config = await flow.GetTopologyConfigAsync(options: new() + { + TopologyName = name + }); + + var topologyId = config?.TopologyId; + if (string.IsNullOrEmpty(topologyId)) + { + return null; + } + - return await flow.GetTopologyAsync(topologyId, options: new() + var param = new Dictionary(options?.Parameters ?? []); + param["agent"] = param.GetValueOrDefault("agent", agent.Name); + param["agent_id"] = param.GetValueOrDefault("agent_id", agent.Id); + param["trigger"] = param.GetValueOrDefault("trigger", trigger.Name); + + return await flow.GetTopologyAsync(topologyId, options: new() + { + Query = options?.Query, + Parameters = param + }); + } + catch (Exception ex) { - AgentId = agent.Id, - TriggerName = trigger.Name, - Query = options?.Query, - Parameters = param - }); + _logger.LogError(ex, $"Error when loading graph (name: {name}, agent: {agent}, trigger: {trigger?.Name})"); + return null; + } } private async Task ExecuteGraphNode( @@ -152,14 +163,18 @@ private async Task ExecuteGraphNode( RuleTriggerOptions? options, List results) { - if (options?.Flow?.TraversalAlgorithm?.IsEqualTo("bfs") == true) - { - await ExecuteGraphNodeBfs(node, graph, agent, trigger, text, states, data, options, results); - } - else + try { - await ExecuteGraphNodeDfs(node, graph, agent, trigger, text, states, data, options, results); + if (options?.Flow?.TraversalAlgorithm?.IsEqualTo("bfs") == true) + { + await ExecuteGraphNodeBfs(node, graph, agent, trigger, text, states, data, options, results); + } + else + { + await ExecuteGraphNodeDfs(node, graph, agent, trigger, text, states, data, options, results); + } } + catch { } } private async Task ExecuteGraphNodeDfs( diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index b85bb2b35..0c64d490a 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -27,7 +27,7 @@ public async Task> GetRuleConfigOptions() foreach (var flow in flows) { var config = await flow.GetTopologyConfigAsync(); - dict[config.TopologyProvider.ToLower()] = config; + dict[config.TopologyName.ToLower()] = config; } return dict; diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index c101966c3..105260448 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -22,7 +22,7 @@ public DemoRuleGraph( _logger = logger; } - public string Provider => "demo"; + public string Name => "One Flow"; public async Task GetTopologyConfigAsync(RuleFlowConfigOptions? options = null) { @@ -30,25 +30,20 @@ public async Task GetTopologyConfigAsync(RuleFlowConfigOptions? var apiKey = settings.ApiKey; var projectId = settings.ProjectId; - var foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Id.IsEqualTo(options?.TopologyId)); - if (foundInstance == null && !string.IsNullOrEmpty(options?.Purpose)) + var topologyName = Name; + if (!string.IsNullOrEmpty(options?.TopologyName)) { - foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Purpose.IsEqualTo(options.Purpose)); - } - - if (foundInstance == null) - { - // default - foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Purpose.IsEqualTo("rule")); + topologyName = options.TopologyName; } + var foundInstance = settings.GraphInstances?.FirstOrDefault(x => x.Name.IsEqualTo(topologyName)); var graphId = foundInstance?.Id ?? string.Empty; var query = Uri.EscapeDataString("MATCH (a)-[r]->(b) WITH a, r, b WHERE a.agent = $agent AND a.trigger = $trigger AND b.agent = $agent AND b.trigger = $trigger RETURN a, r, b LIMIT 100"); return new RuleConfigModel { - TopologyProvider = Provider, TopologyId = graphId, + TopologyName = foundInstance?.Name, CustomParameters = JsonDocument.Parse(JsonSerializer.Serialize(new { htmlTag = "iframe", diff --git a/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs b/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs index dde67a84a..c1370746d 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Settings/MembaseSettings.cs @@ -21,7 +21,7 @@ public class GraphInstance public string Name { get; set; } /// - /// Graph purpose, i.e., rule, etc. + /// Graph description /// - public string Purpose { get; set; } + public string Description { get; set; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index bbcb402cf..b481394a2 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -33,7 +33,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) [BsonIgnoreExtraElements(Inherited = true)] public class RuleConfigMongoModel { - public string? TopologyProvider { get; set; } + public string? TopologyName { get; set; } public static RuleConfigMongoModel? ToMongoModel(RuleConfig? config) { @@ -44,7 +44,7 @@ public class RuleConfigMongoModel return new RuleConfigMongoModel { - TopologyProvider = config.TopologyProvider + TopologyName = config.TopologyName }; } @@ -57,7 +57,7 @@ public class RuleConfigMongoModel return new RuleConfig { - TopologyProvider = config.TopologyProvider + TopologyName = config.TopologyName }; } } From 740fc8599459f049519a2326fa9dd5768474b83d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 16 Mar 2026 10:33:46 -0500 Subject: [PATCH 90/91] minor change --- .../Controllers/Agent/AgentController.Rule.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs index 0c64d490a..43cf228c4 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Rule.cs @@ -27,7 +27,11 @@ public async Task> GetRuleConfigOptions() foreach (var flow in flows) { var config = await flow.GetTopologyConfigAsync(); - dict[config.TopologyName.ToLower()] = config; + if (string.IsNullOrEmpty(config.TopologyName)) + { + continue; + } + dict[config.TopologyName] = config; } return dict; From 5fb97d56e57779d5854a79cfdf99cd7fc4890f8c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 16 Mar 2026 15:03:28 -0500 Subject: [PATCH 91/91] minor change --- .../BotSharp.Abstraction/Models/Gpt4xModelConstants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs b/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs index 2de7a7e68..d934db780 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/Gpt4xModelConstants.cs @@ -10,4 +10,5 @@ public static class Gpt4xModelConstants public const string GPT_4_1_Nano = "gpt-4.1-nano"; public const string GPT_4o_Mini_Realtime_Preview = "gpt-4o-mini-realtime-preview"; public const string GPT_4o_Realtime_Preview = "gpt-4o-realtime-preview"; + public const string GPT_4o_Search_Preview = "gpt-4o-search-preview"; }