From 16300b9a1d855c833d10aceec436d56a31e7e26b Mon Sep 17 00:00:00 2001 From: Visagan Guruparan <103048@smsassist.com> Date: Thu, 10 Jul 2025 14:22:54 -0500 Subject: [PATCH 01/16] Add hook to handle locate element for PlayWright --- .../BotSharp.Abstraction/Browsing/IWebDriverHook.cs | 1 + .../PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs | 5 +++++ .../UtilFunctions/UtilWebLocateElementFn.cs | 1 + 3 files changed, 7 insertions(+) diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs index ce5d03b6..346f2f23 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs @@ -5,4 +5,5 @@ namespace BotSharp.Abstraction.Browsing; public interface IWebDriverHook { Task> GetUploadFiles(MessageInfo message); + Task OnLocateElement(MessageInfo message, string content); } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs index e70d7f27..b4b2eb2f 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.LocateElement.cs @@ -114,6 +114,11 @@ public partial class PlaywrightWebDriver // fix if html has & result.Body = HttpUtility.HtmlDecode(html); result.IsSuccess = true; + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + await hook.OnLocateElement(message, result.Body); + } } else if (count > 1) { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs index d30048c8..e2e3d34f 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebLocateElementFn.cs @@ -30,6 +30,7 @@ public class UtilWebLocateElementFn : IFunctionCallback MessageId = message.MessageId, ContextId = webDriverService.GetMessageContext(message) }; + browser.SetServiceProvider(_services); var result = await browser.LocateElement(msg, locatorArgs); message.Content = $"Locating element {(result.IsSuccess ? "success" : "failed")}. "; From b9c7f63249ea81e2f6b997dbfd3839c0b13032d3 Mon Sep 17 00:00:00 2001 From: Visagan Guruparan <103048@smsassist.com> Date: Thu, 10 Jul 2025 15:55:55 -0500 Subject: [PATCH 02/16] Update contextId for close browser utility. --- .../UtilFunctions/UtilWebCloseBrowserFn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs index ff898232..6c7bfefa 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/UtilFunctions/UtilWebCloseBrowserFn.cs @@ -28,7 +28,7 @@ public class UtilWebCloseBrowserFn : IFunctionCallback ContextId = webDriverService.GetMessageContext(message) }; - await browser.CloseBrowser(message.CurrentAgentId); + await browser.CloseBrowser(msg.ContextId); message.Content = $"Browser closed."; From c80e8ca7a04daf1b7dce003bcd4c29fc45cdb3e3 Mon Sep 17 00:00:00 2001 From: "guoqiang.yu" Date: Fri, 11 Jul 2025 12:59:09 +0800 Subject: [PATCH 03/16] fix #1097 SqlDriver: Database type mssql is not supported --- .../BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs | 2 +- .../BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs | 2 +- src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs | 2 +- .../BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs | 2 +- .../UtilFunctions/GetTableDefinitionFn.cs | 2 +- .../BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs index f571fa6c..c2f0682b 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs @@ -33,7 +33,7 @@ public class ExecuteQueryFn : IFunctionCallback var results = dbType.ToLower() switch { "mysql" => RunQueryInMySql(args.SqlStatements), - "sqlserver" => RunQueryInSqlServer(args.SqlStatements), + "sqlserver" or "mssql" => RunQueryInSqlServer(args.SqlStatements), "redshift" => RunQueryInRedshift(args.SqlStatements), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs index 283620c9..25ef526a 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/GetTableDefinitionFn.cs @@ -37,7 +37,7 @@ public class GetTableDefinitionFn : IFunctionCallback var tableDdls = dbType switch { "mysql" => GetDdlFromMySql(tables), - "sqlserver" => GetDdlFromSqlServer(tables), + "sqlserver" or "mssql" => GetDdlFromSqlServer(tables), "redshift" => GetDdlFromRedshift(tables), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs index eb9253e4..3e693f80 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs @@ -32,7 +32,7 @@ public class SqlSelect : IFunctionCallback var result = dbType switch { "mysql" => RunQueryInMySql(args), - "sqlserver" => RunQueryInSqlServer(args), + "sqlserver" or "mssql" => RunQueryInSqlServer(args), "redshift" => RunQueryInRedshift(args), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs index 89b38f93..1489c4a8 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs @@ -34,7 +34,7 @@ public class SqlValidateFn : IFunctionCallback var validateSql = dbType.ToLower() switch { "mysql" => $"EXPLAIN\r\n{sql.Replace("SET ", "-- SET ", StringComparison.InvariantCultureIgnoreCase).Replace(";", "; EXPLAIN ").TrimEnd("EXPLAIN ".ToCharArray())}", - "sqlserver" => $"SET PARSEONLY ON;\r\n{sql}\r\nSET PARSEONLY OFF;", + "sqlserver" or "mssql" => $"SET PARSEONLY ON;\r\n{sql}\r\nSET PARSEONLY OFF;", "redshift" => $"explain\r\n{sql}", _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs index 20f35572..43005c68 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs @@ -31,7 +31,7 @@ public class GetTableDefinitionFn : IFunctionCallback var tableDdls = dbType switch { "mysql" => GetDdlFromMySql(tables), - "sqlserver" => GetDdlFromSqlServer(tables), + "sqlserver" or "mssql" => GetDdlFromSqlServer(tables), "redshift" => GetDdlFromRedshift(tables,schema), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs index 1ae27ac7..72bef238 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs @@ -30,7 +30,7 @@ public class SqlSelect : IFunctionCallback var result = dbType switch { "mysql" => RunQueryInMySql(args), - "sqlserver" => RunQueryInSqlServer(args), + "sqlserver" or "mssql" => RunQueryInSqlServer(args), "redshift" => RunQueryInRedshift(args), _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; From 2ec304b83c41f10ada6af3b7378e25677c92092c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 11 Jul 2025 10:15:09 -0500 Subject: [PATCH 04/16] refine chat stream request --- .../BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs | 11 +++++------ .../Models/Stream/ChatStreamRequest.cs | 10 ++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs index 7079c642..138d5084 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs @@ -82,8 +82,8 @@ public class ChatStreamMiddleware var (eventType, data) = MapEvents(conn, receivedText); if (eventType == "start") { - var states = InitStates(data); - await ConnectToModel(hub, webSocket, states); + var request = InitRequest(data); + await ConnectToModel(hub, webSocket, request?.States); } else if (eventType == "media") { @@ -157,16 +157,15 @@ public class ChatStreamMiddleware }); } - private List InitStates(string data) + private ChatStreamRequest? InitRequest(string data) { try { - var states = JsonSerializer.Deserialize>(data, BotSharpOptions.defaultJsonOptions); - return states ?? []; + return JsonSerializer.Deserialize(data, BotSharpOptions.defaultJsonOptions); } catch { - return []; + return null; } } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs new file mode 100644 index 00000000..1a72d7dc --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Models/Stream/ChatStreamRequest.cs @@ -0,0 +1,10 @@ +using BotSharp.Abstraction.Models; +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.ChatHub.Models.Stream; + +public class ChatStreamRequest +{ + [JsonPropertyName("states")] + public List States { get; set; } = []; +} From 0d0d808d44e64a070b13fb4b7a5c3e2bfb0baae1 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 11 Jul 2025 11:34:43 -0500 Subject: [PATCH 05/16] add default result --- .../BotSharp.Abstraction/Browsing/IWebDriverHook.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs index 346f2f23..30a58bb4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/IWebDriverHook.cs @@ -4,6 +4,6 @@ namespace BotSharp.Abstraction.Browsing; public interface IWebDriverHook { - Task> GetUploadFiles(MessageInfo message); - Task OnLocateElement(MessageInfo message, string content); + Task> GetUploadFiles(MessageInfo message) => Task.FromResult(new List()); + Task OnLocateElement(MessageInfo message, string content) => Task.CompletedTask; } From a4530093940fd25fd7a465acae8ffb390906754d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 14 Jul 2025 12:40:59 -0500 Subject: [PATCH 06/16] refine message states --- .../Models/MessageState.cs | 8 +++- .../Services/ConversationService.cs | 2 +- .../Hooks/RateLimitConversationHook.cs | 3 -- .../Controllers/TwilioInboundController.cs | 12 +++--- .../Functions/OutboundPhoneCallFn.cs | 39 ++++++++++++------- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs index a8709810..94ab82ea 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs @@ -8,20 +8,24 @@ public class MessageState [JsonPropertyName("active_rounds")] public int ActiveRounds { get; set; } = -1; + [JsonPropertyName("global")] + public bool Global { get; set; } + public MessageState() { } - public MessageState(string key, object value, int activeRounds = -1) + public MessageState(string key, object value, int activeRounds = -1, bool global = false) { Key = key; Value = value; ActiveRounds = activeRounds; + Global = global; } public override string ToString() { - return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}"; + return $"Key: {Key} => Value: {Value}, ActiveRounds: {ActiveRounds}, Global: {Global}"; } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 6eaeb167..676ea362 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -178,7 +178,7 @@ public partial class ConversationService : IConversationService { _conversationId = conversationId; _state.Load(_conversationId, isReadOnly); - states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + states.ForEach(x => _state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, isNeedVersion: !x.Global, source: StateSource.External)); } public async Task GetConversationRecordOrCreateNew(string agentId) diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs index 4752f6d6..02cafbd0 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs @@ -1,9 +1,6 @@ using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Statistics.Enums; -using BotSharp.Abstraction.Statistics.Models; -using BotSharp.Abstraction.Statistics.Services; using BotSharp.Abstraction.Users; namespace BotSharp.Logger.Hooks; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index de58dca6..109db566 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -166,16 +166,16 @@ public class TwilioInboundController : TwilioController var states = new List { - new("channel", ConversationChannel.Phone), - new("calling_phone", request.From), - new("phone_direction", request.Direction), - new("twilio_call_sid", request.CallSid), + new("channel", ConversationChannel.Phone, global: true), + new("calling_phone", request.From, global: true), + new("phone_direction", request.Direction, global: true), + new("twilio_call_sid", request.CallSid, global: true), }; if (request.Direction == "inbound") { - states.Add(new MessageState("calling_phone_from", request.From)); - states.Add(new MessageState("calling_phone_to", request.To)); + states.Add(new MessageState("calling_phone_from", request.From, global: true)); + states.Add(new MessageState("calling_phone_to", request.To, global: true)); } var requestStates = ParseStates(request.States); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 8d34dd22..4f3b2364 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -9,6 +9,7 @@ using BotSharp.Plugin.Twilio.Interfaces; using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; using Twilio.Rest.Api.V2010.Account; +using Twilio.TwiML.Messaging; using Twilio.Types; using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation; using Task = System.Threading.Tasks.Task; @@ -176,7 +177,7 @@ public class OutboundPhoneCallFn : IFunctionCallback }); var utcNow = DateTime.UtcNow; - var excludStates = new List + var excludeStates = new List { "provider", "model", @@ -185,22 +186,34 @@ public class OutboundPhoneCallFn : IFunctionCallback "llm_total_cost" }; - var curStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList(); + var curConvStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList(); var subConvStates = new List { - new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId), - new("channel", "phone"), - new("phone_from", call.From), - new("phone_direction", call.Direction), - new("phone_number", call.To), - new("twilio_call_sid", call.Sid) + new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId, global: true), + new("channel", "phone", global: true), + new("phone_from", call.From, global: true), + new("phone_direction", call.Direction, global: true), + new("phone_number", call.To, global: true), + new("twilio_call_sid", call.Sid, global: true) }; var subStateKeys = subConvStates.Select(x => x.Key).ToList(); - var included = curStates.Where(x => !subStateKeys.Contains(x.Key) && !excludStates.Contains(x.Key)); - var newStates = subConvStates.Concat(included).Select(x => new StateKeyValue + var included = curConvStates.Where(x => !subStateKeys.Contains(x.Key) && !excludeStates.Contains(x.Key)); + + var mappedCurConvStates = MapStates(included, messageId, utcNow); + var mappedSubConvStates = MapStates(subConvStates, messageId, utcNow); + var totalStates = mappedCurConvStates.Concat(mappedSubConvStates).ToList(); + + db.UpdateConversationStates(newConversationId, totalStates); + } + + private IEnumerable MapStates(IEnumerable states, string messageId, DateTime updateTime) + { + if (states.IsNullOrEmpty()) return []; + + return states.Select(x => new StateKeyValue { Key = x.Key, - Versioning = true, + Versioning = !x.Global, Values = [ new StateValue { @@ -209,11 +222,9 @@ public class OutboundPhoneCallFn : IFunctionCallback Active = true, ActiveRounds = x.ActiveRounds, Source = StateSource.Application, - UpdateTime = utcNow + UpdateTime = updateTime } ] }).ToList(); - - db.UpdateConversationStates(newConversationId, newStates); } } From 85a87eda7a0d9ba7ebed26dcd0c413d9c4b9add7 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 14 Jul 2025 12:51:59 -0500 Subject: [PATCH 07/16] rename --- .../BotSharp.Abstraction/Models/MessageState.cs | 4 ++-- .../Controllers/TwilioInboundController.cs | 12 ++++++------ .../Functions/OutboundPhoneCallFn.cs | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs index 94ab82ea..074be848 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Models/MessageState.cs @@ -16,12 +16,12 @@ public class MessageState } - public MessageState(string key, object value, int activeRounds = -1, bool global = false) + public MessageState(string key, object value, int activeRounds = -1, bool isGlobal = false) { Key = key; Value = value; ActiveRounds = activeRounds; - Global = global; + Global = isGlobal; } public override string ToString() diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index 109db566..b5dbfb2e 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -166,16 +166,16 @@ public class TwilioInboundController : TwilioController var states = new List { - new("channel", ConversationChannel.Phone, global: true), - new("calling_phone", request.From, global: true), - new("phone_direction", request.Direction, global: true), - new("twilio_call_sid", request.CallSid, global: true), + new("channel", ConversationChannel.Phone, isGlobal: true), + new("calling_phone", request.From, isGlobal: true), + new("phone_direction", request.Direction, isGlobal: true), + new("twilio_call_sid", request.CallSid, isGlobal: true), }; if (request.Direction == "inbound") { - states.Add(new MessageState("calling_phone_from", request.From, global: true)); - states.Add(new MessageState("calling_phone_to", request.To, global: true)); + states.Add(new MessageState("calling_phone_from", request.From, isGlobal: true)); + states.Add(new MessageState("calling_phone_to", request.To, isGlobal: true)); } var requestStates = ParseStates(request.States); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 4f3b2364..02f000c2 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -189,12 +189,12 @@ public class OutboundPhoneCallFn : IFunctionCallback var curConvStates = state.GetStates().Select(x => new MessageState(x.Key, x.Value)).ToList(); var subConvStates = new List { - new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId, global: true), - new("channel", "phone", global: true), - new("phone_from", call.From, global: true), - new("phone_direction", call.Direction, global: true), - new("phone_number", call.To, global: true), - new("twilio_call_sid", call.Sid, global: true) + new(StateConst.ORIGIN_CONVERSATION_ID, originConversationId, isGlobal: true), + new("channel", "phone", isGlobal: true), + new("phone_from", call.From, isGlobal: true), + new("phone_direction", call.Direction, isGlobal: true), + new("phone_number", call.To, isGlobal: true), + new("twilio_call_sid", call.Sid, isGlobal: true) }; var subStateKeys = subConvStates.Select(x => x.Key).ToList(); var included = curConvStates.Where(x => !subStateKeys.Contains(x.Key) && !excludeStates.Contains(x.Key)); From 0198fbccbbae068b30f021a99c96e6591d19c64d Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 14 Jul 2025 12:54:20 -0500 Subject: [PATCH 08/16] minor change --- .../OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 02f000c2..3c5532bd 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -201,9 +201,9 @@ public class OutboundPhoneCallFn : IFunctionCallback var mappedCurConvStates = MapStates(included, messageId, utcNow); var mappedSubConvStates = MapStates(subConvStates, messageId, utcNow); - var totalStates = mappedCurConvStates.Concat(mappedSubConvStates).ToList(); + var allStates = mappedCurConvStates.Concat(mappedSubConvStates).ToList(); - db.UpdateConversationStates(newConversationId, totalStates); + db.UpdateConversationStates(newConversationId, allStates); } private IEnumerable MapStates(IEnumerable states, string messageId, DateTime updateTime) From 3d1c3c0daa5e055b6230bbce96bd57970873dd6a Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 14 Jul 2025 16:59:09 -0500 Subject: [PATCH 09/16] refine conv filter --- .../Repositories/Filters/ConversationFilter.cs | 1 + .../FileRepository/FileRepository.Conversation.cs | 10 ++++++++-- .../Repository/MongoRepository.Conversation.cs | 10 ++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs index 9ba98c5d..b94876c3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs @@ -10,6 +10,7 @@ public class ConversationFilter public string? Title { get; set; } public string? TitleAlias { get; set; } public string? AgentId { get; set; } + public List? AgentIds { get; set; } public string? Status { get; set; } public string? Channel { get; set; } public string? ChannelId { get; set; } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 3d03e816..f772acb2 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -396,6 +396,12 @@ public partial class FileRepository Directory.CreateDirectory(dir); } + if (filter?.AgentId != null) + { + filter.AgentIds ??= []; + filter.AgentIds.Add(filter.AgentId); + } + var totalDirs = Directory.GetDirectories(dir); foreach (var d in totalDirs) { @@ -419,9 +425,9 @@ public partial class FileRepository { matched = matched && record.TitleAlias.Contains(filter.TitleAlias); } - if (filter?.AgentId != null) + if (filter?.AgentIds != null && filter.AgentIds.Any()) { - matched = matched && record.AgentId == filter.AgentId; + matched = matched && filter.AgentIds.Contains(record.AgentId); } if (filter?.Status != null) { diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index edcf3a5f..d0da6fbf 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -346,6 +346,12 @@ public partial class MongoRepository var convBuilder = Builders.Filter; var convFilters = new List>() { convBuilder.Empty }; + if (filter?.AgentId != null) + { + filter.AgentIds ??= []; + filter.AgentIds.Add(filter.AgentId); + } + // Filter conversations if (!string.IsNullOrEmpty(filter?.Id)) { @@ -359,9 +365,9 @@ public partial class MongoRepository { convFilters.Add(convBuilder.Regex(x => x.Title, new BsonRegularExpression(filter.TitleAlias, "i"))); } - if (!string.IsNullOrEmpty(filter?.AgentId)) + if (filter?.AgentIds != null && filter.AgentIds.Any()) { - convFilters.Add(convBuilder.Eq(x => x.AgentId, filter.AgentId)); + convFilters.Add(convBuilder.In(x => x.AgentId, filter.AgentIds)); } if (!string.IsNullOrEmpty(filter?.Status)) { From 1662dde4196c7f4f7616b25935ed60293d8bd3f8 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 14 Jul 2025 17:31:03 -0500 Subject: [PATCH 10/16] minor change --- .../SideCar/Models/SideCarOptions.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs index a4858c5c..f221cab2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Models/SideCarOptions.cs @@ -7,6 +7,15 @@ public class SideCarOptions public static SideCarOptions Empty() { - return new SideCarOptions(); + return new(); + } + + public static SideCarOptions InheritStates(IEnumerable? targetStates = null) + { + return new() + { + IsInheritStates = true, + InheritStateKeys = targetStates + }; } } From 95fd02be200a42ac881906c99455bbe538f7bc0d Mon Sep 17 00:00:00 2001 From: Visagan Guruparan <103048@smsassist.com> Date: Mon, 21 Jul 2025 16:02:40 -0500 Subject: [PATCH 11/16] add channel to the instruct controller --- .../BotSharp.OpenAPI/Controllers/InstructModeController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs index e8c87c2c..de67fcab 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/InstructModeController.cs @@ -30,7 +30,8 @@ public class InstructModeController : ControllerBase .SetState("model_id", input.ModelId, source: StateSource.External) .SetState("instruction", input.Instruction, source: StateSource.External) .SetState("input_text", input.Text, source: StateSource.External) - .SetState("template_name", input.Template, source: StateSource.External); + .SetState("template_name", input.Template, source: StateSource.External) + .SetState("channel", input.Channel, source: StateSource.External); var instructor = _services.GetRequiredService(); var result = await instructor.Execute(agentId, From 061ce95e353d05622da935a3a57b6bdff57a2d0b Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 21 Jul 2025 23:01:53 -0500 Subject: [PATCH 12/16] allow using template across agents --- .../FileRepository/FileRepository.Agent.cs | 2 +- .../Templating/TemplateRender.cs | 46 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index f7e70ca7..c5947708 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -475,7 +475,7 @@ namespace BotSharp.Core.Repository if (!filter.AgentNames.IsNullOrEmpty()) { - query = query.Where(x => filter.AgentNames.Contains(x.Name)); + query = query.Where(x => filter.AgentNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase)); } if (!string.IsNullOrEmpty(filter.SimilarName)) diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index f7e5ecde..5b65465d 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -8,6 +8,7 @@ using System.Collections; using System.IO; using System.Reflection; using System.Text.Encodings.Web; +using System.Text.RegularExpressions; namespace BotSharp.Core.Templating; @@ -36,9 +37,9 @@ public class TemplateRender : ITemplateRender _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); - _parser.RegisterIdentifierTag("link", (string identifier, TextWriter writer, TextEncoder encoder, TemplateContext context) => + _parser.RegisterExpressionTag("link", (Expression expression, TextWriter writer, TextEncoder encoder, TemplateContext context) => { - return RenderIdentifierTag("link", identifier, writer, encoder, context); + return RenderTag("link", expression, writer, encoder, context, services); }); } @@ -82,20 +83,47 @@ public class TemplateRender : ITemplateRender #region Private methods - private static async ValueTask RenderIdentifierTag(string tag, string identifier, TextWriter writer, TextEncoder encoder, TemplateContext context) + private static async ValueTask RenderTag( + string tag, + Expression expression, + TextWriter writer, + TextEncoder encoder, + TemplateContext context, + IServiceProvider services) { + try { - var value = await context.Model.GetValueAsync(TemplateRenderConstant.RENDER_AGENT, context); - var agent = value?.ToObjectValue() as Agent; - var found = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(identifier)); - var key = $"{agent?.Id} | {tag} | {identifier}"; + var value = await expression.EvaluateAsync(context); + var expStr = value?.ToStringValue() ?? string.Empty; - if (found == null || (context.AmbientValues.TryGetValue(key, out var visited) && (bool)visited)) + value = await context.Model.GetValueAsync(TemplateRenderConstant.RENDER_AGENT, context); + var agent = value?.ToObjectValue() as Agent; + + var splited = Regex.Split(expStr, @"\s*from\s*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .ToArray(); + + var templateName = splited.ElementAtOrDefault(0); + var agentName = splited.ElementAtOrDefault(1); + + if (splited.Length > 1 && !agentName.IsEqualTo(agent?.Name)) + { + using var scope = services.CreateScope(); + var agentService = scope.ServiceProvider.GetRequiredService(); + var result = await agentService.GetAgents(new() { SimilarName = agentName }); + agent = result?.Items?.FirstOrDefault(); + } + + var template = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(templateName)); + var key = $"{tag} | {agent?.Id} | {templateName}"; + + if (template == null || (context.AmbientValues.TryGetValue(key, out var visited) && (bool)visited)) { writer.Write(string.Empty); } - else if (_parser.TryParse(found.Content, out var t, out _)) + else if (_parser.TryParse(template.Content, out var t, out _)) { context.AmbientValues[key] = true; var rendered = t.Render(context); From 1cabd91ec083fdce331a562e58c3a07db94ce180 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 21 Jul 2025 23:02:44 -0500 Subject: [PATCH 13/16] minor change --- src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index 5b65465d..e2a0d029 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -91,7 +91,6 @@ public class TemplateRender : ITemplateRender TemplateContext context, IServiceProvider services) { - try { var value = await expression.EvaluateAsync(context); From b1eb7543d411025e287e02ee77d33eaf36326eaf Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Tue, 22 Jul 2025 10:26:00 -0500 Subject: [PATCH 14/16] add from agent filter --- .../BotSharp.Core/Templating/TemplateRender.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index e2a0d029..2124da16 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -4,6 +4,7 @@ using BotSharp.Abstraction.Templating; using BotSharp.Abstraction.Translation.Models; using Fluid; using Fluid.Ast; +using Fluid.Values; using System.Collections; using System.IO; using System.Reflection; @@ -37,6 +38,8 @@ public class TemplateRender : ITemplateRender _options.MemberAccessStrategy.Register(); _options.MemberAccessStrategy.Register(); + _options.Filters.AddFilter("from_agent", FromAgentFilter); + _parser.RegisterExpressionTag("link", (Expression expression, TextWriter writer, TextEncoder encoder, TemplateContext context) => { return RenderTag("link", expression, writer, encoder, context, services); @@ -99,7 +102,7 @@ public class TemplateRender : ITemplateRender value = await context.Model.GetValueAsync(TemplateRenderConstant.RENDER_AGENT, context); var agent = value?.ToObjectValue() as Agent; - var splited = Regex.Split(expStr, @"\s*from\s*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) + var splited = Regex.Split(expStr, @"\s*from_agent\s*", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .ToArray(); @@ -142,6 +145,16 @@ public class TemplateRender : ITemplateRender return Completion.Normal; } + private static ValueTask FromAgentFilter( + FluidValue input, + FilterArguments arguments, + TemplateContext context) + { + var inputStr = input?.ToStringValue() ?? string.Empty; + var fromAgent = arguments.At(0).ToStringValue(); + return new StringValue($"{inputStr} from_agent {fromAgent}"); + } + private static bool IsStringType(Type type) { return type == typeof(string); From ee491369b0126f77f1e04eaeb5606756b60e8305 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 23 Jul 2025 15:29:44 -0500 Subject: [PATCH 15/16] update router instruction --- .../instructions/instruction.liquid | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid index 775de66c..341557bb 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid @@ -5,7 +5,8 @@ Follow these steps to handle user request: 2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service. 3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments. 4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared. -5. If user is greeting, you can call function response_to_user with a greeting message. +5. Call function route_to_agent if user have specific requests, let agent handle and fill the required args, do not ask user to provide any args by yourself. +6. If user is greeting or do not have specific request, you can call function response_to_user with a greeting message. {% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS] From 27dc42df8a058c23807afd1a8b348a6bfccbcc37 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Wed, 23 Jul 2025 15:40:18 -0500 Subject: [PATCH 16/16] update router instruction --- .../instructions/instruction.liquid | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid index 341557bb..1ada1850 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid @@ -5,8 +5,8 @@ Follow these steps to handle user request: 2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service. 3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments. 4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared. -5. Call function route_to_agent if user have specific requests, let agent handle and fill the required args, do not ask user to provide any args by yourself. -6. If user is greeting or do not have specific request, you can call function response_to_user with a greeting message. +5. Call function route_to_agent if user have specific requests and available agent to proceed. Do not ask user to provide any required args by yourself. The requested agent will handle and fill the required args internally. +6. If user is greeting or do not have specific request, then you can call function response_to_user with a greeting message. {% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS]