From fd667184f812cbb81cc132c5145af9eb17cade0e Mon Sep 17 00:00:00 2001 From: "aden.chen" Date: Tue, 4 Nov 2025 11:18:24 +0800 Subject: [PATCH 1/5] Supports allow to interrupt the welcome message playing --- .../Controllers/TwilioOutboundController.cs | 10 +++++++++- .../Models/ConversationalVoiceRequest.cs | 3 +++ .../Functions/OutboundPhoneCallFn.cs | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs index 5543af38a..5fb28e3f5 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs @@ -53,7 +53,15 @@ await HookEmitter.Emit(_services, { instruction.SpeechPaths.Add(request.InitAudioFile); } - response = twilio.ReturnNoninterruptedInstructions(instruction); + + if (request.WelcomeMessageAllowToBeInterrupted.HasValue && request.WelcomeMessageAllowToBeInterrupted.Value) + { + response = twilio.ReturnInstructions(instruction); + } + else + { + response = twilio.ReturnNoninterruptedInstructions(instruction); + } } return TwiML(response); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs index 344ddc06f..5a7c4adf2 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs @@ -42,6 +42,9 @@ public class ConversationalVoiceRequest : VoiceRequest [FromForm] public int MachineDetectionDuration { get; set; } + [FromQuery(Name = "welcome_message_allow_to_be_interrupted")] + public bool? WelcomeMessageAllowToBeInterrupted { get; set; } + [FromForm] public int CallDuration { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 3c5532bdb..5374e05cc 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -25,6 +25,7 @@ public class OutboundPhoneCallFn : IFunctionCallback public string Name => "util-twilio-outbound_phone_call"; public string Indication => "Dialing the phone number"; + public const string WelcomeMessageAllowToBeInterrupted = "welcome_message_allow_to_be_interrupted"; public OutboundPhoneCallFn( IServiceProvider services, @@ -106,6 +107,11 @@ public async Task Execute(RoleDialogModel message) processUrl += $"&init-audio-file={initAudioFile}"; } + if (agent.Labels.Contains(WelcomeMessageAllowToBeInterrupted)) + { + processUrl += $"${WelcomeMessageAllowToBeInterrupted}=true"; + } + // Make outbound call var call = await CallResource.CreateAsync( url: new Uri(processUrl), From 573cbb4419e4a398b382203c7077f5130589bc15 Mon Sep 17 00:00:00 2001 From: "aden.chen" Date: Tue, 4 Nov 2025 11:33:54 +0800 Subject: [PATCH 2/5] modify key of WelcomeMessageAllowToBeInterrupted --- .../BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs | 2 +- .../OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs index 5a7c4adf2..550ff5fd0 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs @@ -42,7 +42,7 @@ public class ConversationalVoiceRequest : VoiceRequest [FromForm] public int MachineDetectionDuration { get; set; } - [FromQuery(Name = "welcome_message_allow_to_be_interrupted")] + [FromQuery(Name = "welcome_msg_allow_interrupt")] public bool? WelcomeMessageAllowToBeInterrupted { get; set; } [FromForm] diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 5374e05cc..4bdf0e23a 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -25,7 +25,7 @@ public class OutboundPhoneCallFn : IFunctionCallback public string Name => "util-twilio-outbound_phone_call"; public string Indication => "Dialing the phone number"; - public const string WelcomeMessageAllowToBeInterrupted = "welcome_message_allow_to_be_interrupted"; + public const string WelcomeMessageAllowToBeInterrupted = "welcome_msg_allow_interrupt"; public OutboundPhoneCallFn( IServiceProvider services, From 5bcf757b18fea4041a34d81bcee69047670ee3aa Mon Sep 17 00:00:00 2001 From: "aden.chen" Date: Thu, 4 Dec 2025 09:08:20 +0800 Subject: [PATCH 3/5] new crontab api for scheduling every one minute --- .../Crontab/Models/CrontabItem.cs | 6 ++ .../Services/CrontabItemExtension.cs | 26 +++++++++ .../BotSharp.OpenAPI/BotSharp.OpenAPI.csproj | 3 +- .../Controllers/Crontab/CrontabController.cs | 56 ++++++++++++++++--- 4 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs index 3a5310125..02db4534a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs @@ -29,6 +29,12 @@ public class CrontabItem : ScheduleTaskArgs [JsonPropertyName("created_time")] public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + [JsonPropertyName("trigger_by_watcher")] + public bool TriggerByWatcher { get; set; } = true; + + [JsonPropertyName("trigger_by_openapi")] + public bool TriggerByOpenAPI { get; set; } + public override string ToString() { return $"{Title}: {Description} [AgentId: {AgentId}, UserId: {UserId}]"; diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs new file mode 100644 index 000000000..b7240e529 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs @@ -0,0 +1,26 @@ +using NCrontab; + +namespace BotSharp.Core.Crontab.Services +{ + public static class CrontabItemExtension + { + public static bool CheckNextOccurrenceEveryOneMinute(this CrontabItem item) + { + // strip seconds from cron expression + item.Cron = string.Join(" ", item.Cron.Split(' ').TakeLast(5)); + var schedule = CrontabSchedule.Parse(item.Cron, new CrontabSchedule.ParseOptions + { + IncludingSeconds = false // Ensure you account for seconds + }); + + var currentTime = DateTime.UtcNow; + + // Check if there has been an execution point within the past minute. + var oneMinuteAgo = currentTime.AddMinutes(-1); + var nextOccurrenceFromPast = schedule.GetNextOccurrence(oneMinuteAgo); + + // If the next execution point falls within the past minute up to the present, then it matches. + return nextOccurrenceFromPast > oneMinuteAgo && nextOccurrenceFromPast <= currentTime; + } + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj index e2b453edb..41fcf07dc 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -35,6 +35,7 @@ + diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs index 3f3127789..b8d69b6ea 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs @@ -1,4 +1,6 @@ using BotSharp.Abstraction.Crontab; +using BotSharp.Abstraction.Crontab.Models; +using BotSharp.Core.Crontab.Services; namespace BotSharp.OpenAPI.Controllers; @@ -20,25 +22,63 @@ public CrontabController( [HttpPost("/crontab/{name}")] public async Task RunCrontab(string name) { - var cron = _services.GetRequiredService(); - var crons = await cron.GetCrontable(); - var found = crons.FirstOrDefault(x => x.Title.IsEqualTo(name)); - if (found == null) + var found = await GetCrontabItems(name); + if (found.IsNullOrEmpty()) { _logger.LogWarning($"Cannnot find crontab {name}"); return false; } + return await ExecuteTimeArrivedItem(found.First()); + } + + /// + /// As the Dkron job trigger API, run every 1 minutes + /// + /// + [HttpPost("/crontab/scheduling-per-minute")] + public async Task SchedulingCrontab() + { + var allowedCrons = await GetCrontabItems(); + + foreach (var item in allowedCrons) + { + if (item.CheckNextOccurrenceEveryOneMinute()) + { + _logger.LogInformation("Crontab: {0}, One occurrence was matched, Beginning execution...", item.Title); + Task.Run(() => ExecuteTimeArrivedItem(item)); + } + } + } + + private async Task> GetCrontabItems(string? title = null) + { + var crontabService = _services.GetRequiredService(); + var crons = await crontabService.GetCrontable(); + var allowedCrons = crons.Where(cron => cron.TriggerByOpenAPI).ToList(); + + if (title is null) + { + return allowedCrons; + } + + return allowedCrons.Where(cron => cron.Title.IsEqualTo(title)).ToList(); + } + + private async Task ExecuteTimeArrivedItem(CrontabItem item) + { try { - _logger.LogWarning($"Start running crontab {name}"); - await cron.ScheduledTimeArrived(found); - _logger.LogWarning($"Complete running crontab {name}"); + using var scope = _services.CreateScope(); + var crontabService = scope.ServiceProvider.GetRequiredService(); + _logger.LogWarning($"Start running crontab {item.Title}"); + await crontabService.ScheduledTimeArrived(item); + _logger.LogWarning($"Complete running crontab {item.Title}"); return true; } catch (Exception ex) { - _logger.LogError(ex, $"Error when running crontab {name}"); + _logger.LogError(ex, $"Error when running crontab {item.Title}"); return false; } } From 2f3cbbe4c7617a39c1ebf74d315b6b4d4f80c8f7 Mon Sep 17 00:00:00 2001 From: "aden.chen" Date: Thu, 4 Dec 2025 09:30:37 +0800 Subject: [PATCH 4/5] Revert "Supports allow to interrupt the welcome message playing" --- .../Controllers/TwilioOutboundController.cs | 10 +--------- .../Models/ConversationalVoiceRequest.cs | 3 --- .../Functions/OutboundPhoneCallFn.cs | 6 ------ 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs index 5fb28e3f5..5543af38a 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs @@ -53,15 +53,7 @@ await HookEmitter.Emit(_services, { instruction.SpeechPaths.Add(request.InitAudioFile); } - - if (request.WelcomeMessageAllowToBeInterrupted.HasValue && request.WelcomeMessageAllowToBeInterrupted.Value) - { - response = twilio.ReturnInstructions(instruction); - } - else - { - response = twilio.ReturnNoninterruptedInstructions(instruction); - } + response = twilio.ReturnNoninterruptedInstructions(instruction); } return TwiML(response); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs index 550ff5fd0..344ddc06f 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs @@ -42,9 +42,6 @@ public class ConversationalVoiceRequest : VoiceRequest [FromForm] public int MachineDetectionDuration { get; set; } - [FromQuery(Name = "welcome_msg_allow_interrupt")] - public bool? WelcomeMessageAllowToBeInterrupted { get; set; } - [FromForm] public int CallDuration { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs index 4bdf0e23a..3c5532bdb 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs @@ -25,7 +25,6 @@ public class OutboundPhoneCallFn : IFunctionCallback public string Name => "util-twilio-outbound_phone_call"; public string Indication => "Dialing the phone number"; - public const string WelcomeMessageAllowToBeInterrupted = "welcome_msg_allow_interrupt"; public OutboundPhoneCallFn( IServiceProvider services, @@ -107,11 +106,6 @@ public async Task Execute(RoleDialogModel message) processUrl += $"&init-audio-file={initAudioFile}"; } - if (agent.Labels.Contains(WelcomeMessageAllowToBeInterrupted)) - { - processUrl += $"${WelcomeMessageAllowToBeInterrupted}=true"; - } - // Make outbound call var call = await CallResource.CreateAsync( url: new Uri(processUrl), From 21d26fa046f27d60864711ae3593e3aaec133451 Mon Sep 17 00:00:00 2001 From: "aden.chen" Date: Thu, 4 Dec 2025 11:36:49 +0800 Subject: [PATCH 5/5] refactor --- .../Services/CrontabItemExtension.cs | 10 +++++----- .../Services/CrontabWatcher.cs | 3 ++- .../Controllers/Crontab/CrontabController.cs | 19 ++++++++++++++++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs index b7240e529..1be4cd93f 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabItemExtension.cs @@ -14,13 +14,13 @@ public static bool CheckNextOccurrenceEveryOneMinute(this CrontabItem item) }); var currentTime = DateTime.UtcNow; + var currentMinute = new DateTime(currentTime.Year, currentTime.Month, currentTime.Day, + currentTime.Hour, currentTime.Minute, 0, DateTimeKind.Utc); - // Check if there has been an execution point within the past minute. - var oneMinuteAgo = currentTime.AddMinutes(-1); - var nextOccurrenceFromPast = schedule.GetNextOccurrence(oneMinuteAgo); + var oneMinuteAgo = currentMinute.AddMinutes(-1); + var nextOccurrence = schedule.GetNextOccurrence(oneMinuteAgo); - // If the next execution point falls within the past minute up to the present, then it matches. - return nextOccurrenceFromPast > oneMinuteAgo && nextOccurrenceFromPast <= currentTime; + return nextOccurrence == currentMinute; } } } diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs index dddd3d58a..ea1798d43 100644 --- a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs @@ -58,10 +58,11 @@ private async Task RunCronChecker(IServiceProvider services) { var cron = services.GetRequiredService(); var crons = await cron.GetCrontable(); + var allowedCrons = crons.Where(cron => cron.TriggerByWatcher).ToList(); var settings = services.GetRequiredService(); var publisher = services.GetService(); - foreach (var item in crons) + foreach (var item in allowedCrons) { _logger.LogDebug($"[{DateTime.UtcNow}] Cron task ({item.Title}, {item.Cron}), Last Execution Time: {item.LastExecutionTime}"); diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs index b8d69b6ea..2f1e67d03 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Crontab/CrontabController.cs @@ -22,14 +22,27 @@ public CrontabController( [HttpPost("/crontab/{name}")] public async Task RunCrontab(string name) { - var found = await GetCrontabItems(name); - if (found.IsNullOrEmpty()) + var cron = _services.GetRequiredService(); + var crons = await cron.GetCrontable(); + var found = crons.FirstOrDefault(x => x.Title.IsEqualTo(name)); + if (found == null) { _logger.LogWarning($"Cannnot find crontab {name}"); return false; } - return await ExecuteTimeArrivedItem(found.First()); + try + { + _logger.LogWarning($"Start running crontab {name}"); + await cron.ScheduledTimeArrived(found); + _logger.LogWarning($"Complete running crontab {name}"); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when running crontab {name}"); + return false; + } } ///