From c78556233091684bd2adabc63b5f8e2e35e42c87 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Wed, 28 Aug 2024 10:02:53 -0500 Subject: [PATCH 1/8] Disable RateLimit for phone. --- .../Hooks/RateLimitConversationHook.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs index 158257be..1cb09f3e 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Users; @@ -47,20 +48,26 @@ public class RateLimitConversationHook : ConversationHookBase } } - // Check the number of conversations - var user = _services.GetRequiredService(); - var convService = _services.GetRequiredService(); - var results = await convService.GetConversations(new ConversationFilter - { - UserId = user.Id, - StartTime = DateTime.UtcNow.AddHours(-24), - }); + var states = _services.GetRequiredService(); + var channel = states.GetState("channel"); - if (results.Count > rateLimit.MaxConversationPerDay) + // Check the number of conversations + if (channel != ConversationChannel.Phone && channel != ConversationChannel.Email) { - message.Content = $"The number of conversations you have exceeds the system maximum of {rateLimit.MaxConversationPerDay}"; - message.StopCompletion = true; - return; + var user = _services.GetRequiredService(); + var convService = _services.GetRequiredService(); + var results = await convService.GetConversations(new ConversationFilter + { + UserId = user.Id, + StartTime = DateTime.UtcNow.AddHours(-24), + }); + + if (results.Count > rateLimit.MaxConversationPerDay) + { + message.Content = $"The number of conversations you have exceeds the system maximum of {rateLimit.MaxConversationPerDay}"; + message.StopCompletion = true; + return; + } } } } From 5801bdfa3e7afc39dd7cff7624f5f4224b234a82 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Wed, 28 Aug 2024 15:03:23 -0500 Subject: [PATCH 2/8] remove hard-code hold-on response. --- .../Controllers/TwilioVoiceController.cs | 58 ++++++++++--------- .../Services/TwilioService.cs | 10 ++-- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index a89c6464..d2914b3b 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -4,6 +4,7 @@ using BotSharp.Plugin.Twilio.Models; using BotSharp.Plugin.Twilio.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Twilio.Http; namespace BotSharp.Plugin.Twilio.Controllers; @@ -35,7 +36,7 @@ public class TwilioVoiceController : TwilioController string conversationId = $"TwilioVoice_{request.CallSid}"; var twilio = _services.GetRequiredService(); var url = $"twilio/voice/{conversationId}/receive/0?states={states}"; - var response = twilio.ReturnInstructions("twilio/welcome.mp3", url, true); + var response = twilio.ReturnInstructions(new List { "twilio/welcome.mp3" }, url, true, timeout: 1); return TwiML(response); } @@ -53,17 +54,10 @@ public class TwilioVoiceController : TwilioController messages.Add(text); await sessionManager.StageCallerMessageAsync(conversationId, seqNum, text); } + VoiceResponse response; - if (messages.Count == 0 && seqNum == 0) - { - response = twilio.ReturnInstructions("twilio/welcome.mp3", $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true, timeout: 2); - } - else + if (messages.Any()) { - if (messages.Count == 0) - { - messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum - 1); - } var messageContent = string.Join("\r\n", messages); var callerMessage = new CallerMessage() { @@ -82,9 +76,14 @@ public class TwilioVoiceController : TwilioController } await messageQueue.EnqueueAsync(callerMessage); - int audioIndex = Random.Shared.Next(1, 5); - response = twilio.ReturnInstructions($"twilio/hold-on-{audioIndex}.mp3", $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 1); + response = new VoiceResponse() + .Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post); } + else + { + response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true); + } + return TwiML(response); } @@ -106,25 +105,30 @@ public class TwilioVoiceController : TwilioController var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum); if (indication != null) { - string speechPath; - if (indication.StartsWith('#')) + var speechPaths = new List(); + foreach (var text in indication.Split('|')) { - speechPath = $"twilio/{indication.Substring(1)}"; + var seg = text.Trim(); + if (seg.StartsWith('#')) + { + speechPaths.Add($"twilio/{seg.Substring(1)}.mp3"); + } + else + { + var textToSpeechService = CompletionProvider.GetTextToSpeech(_services, "openai", "tts-1"); + var fileService = _services.GetRequiredService(); + var data = await textToSpeechService.GenerateSpeechFromTextAsync(seg); + var fileName = $"indication_{seqNum}.mp3"; + await fileService.SaveSpeechFileAsync(conversationId, fileName, data); + speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}"); + } } - else - { - var textToSpeechService = CompletionProvider.GetTextToSpeech(_services, "openai", "tts-1"); - var fileService = _services.GetRequiredService(); - var data = await textToSpeechService.GenerateSpeechFromTextAsync(indication); - var fileName = $"indication_{seqNum}.mp3"; - await fileService.SaveSpeechFileAsync(conversationId, fileName, data); - speechPath = $"twilio/voice/speeches/{conversationId}/{fileName}"; - } - response = twilio.ReturnInstructions(speechPath, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 2); + response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); } else { - response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 1); + int audioIndex = Random.Shared.Next(1, 4); + response = twilio.ReturnInstructions(new List { $"{_settings.CallbackHost}/twilio/typing-{audioIndex}.mp3" }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 1); } } else @@ -135,7 +139,7 @@ public class TwilioVoiceController : TwilioController } else { - response = twilio.ReturnInstructions($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}", $"twilio/voice/{conversationId}/receive/{nextSeqNum}?states={states}", true); + response = twilio.ReturnInstructions(new List { $"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}" }, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?states={states}", true); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index 309d627c..5ccdc7db 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -64,7 +64,7 @@ public class TwilioService return response; } - public VoiceResponse ReturnInstructions(string speechPath, string callbackPath, bool actionOnEmptyResult, int timeout = 3) + public VoiceResponse ReturnInstructions(List speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 2) { var response = new VoiceResponse(); var gather = new Gather() @@ -80,13 +80,11 @@ public class TwilioService Timeout = timeout > 0 ? timeout : 3, ActionOnEmptyResult = actionOnEmptyResult }; - if (!string.IsNullOrEmpty(speechPath)) + if (speechPaths != null && speechPaths.Any()) { - gather.Play(new Uri($"{_settings.CallbackHost}/{speechPath}")); - if (speechPath.Contains("hold-on-")) + foreach (var speechPath in speechPaths) { - int audioIndex = Random.Shared.Next(1, 4); - gather.Play(new Uri($"{_settings.CallbackHost}/twilio/typing-{audioIndex}.mp3")); + gather.Play(new Uri($"{_settings.CallbackHost}/{speechPath}")); } } response.Append(gather); From 06973d89888f2b7b399a35e8d400bfb74669e74e Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Wed, 28 Aug 2024 16:19:47 -0500 Subject: [PATCH 3/8] Support play for reply --- .../Controllers/TwilioVoiceController.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index d2914b3b..db611254 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -76,8 +76,9 @@ public class TwilioVoiceController : TwilioController } await messageQueue.EnqueueAsync(callerMessage); + int audioIndex = Random.Shared.Next(2, 5); response = new VoiceResponse() - .Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post); + .Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}&play=%23hold-on-{audioIndex}%7c%23typing-2"), HttpMethod.Post); } else { @@ -89,7 +90,8 @@ public class TwilioVoiceController : TwilioController [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")] - public async Task ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request) + public async Task ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, + [FromQuery] string states, [FromQuery] string play, VoiceRequest request) { var nextSeqNum = seqNum + 1; var sessionManager = _services.GetRequiredService(); @@ -102,7 +104,9 @@ public class TwilioVoiceController : TwilioController VoiceResponse response; if (reply == null) { - var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum); + var indication = string.IsNullOrEmpty(play) ? + await sessionManager.GetReplyIndicationAsync(conversationId, seqNum) : + play; if (indication != null) { var speechPaths = new List(); From 841f8a68dd7b04967484ee191d4c32dfcfcdb86c Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Wed, 28 Aug 2024 16:42:55 -0500 Subject: [PATCH 4/8] change to 2 seconds. --- .../Controllers/TwilioVoiceController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index db611254..b0745b3d 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -36,7 +36,7 @@ public class TwilioVoiceController : TwilioController string conversationId = $"TwilioVoice_{request.CallSid}"; var twilio = _services.GetRequiredService(); var url = $"twilio/voice/{conversationId}/receive/0?states={states}"; - var response = twilio.ReturnInstructions(new List { "twilio/welcome.mp3" }, url, true, timeout: 1); + var response = twilio.ReturnInstructions(new List { "twilio/welcome.mp3" }, url, true); return TwiML(response); } @@ -132,7 +132,7 @@ public class TwilioVoiceController : TwilioController else { int audioIndex = Random.Shared.Next(1, 4); - response = twilio.ReturnInstructions(new List { $"{_settings.CallbackHost}/twilio/typing-{audioIndex}.mp3" }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 1); + response = twilio.ReturnInstructions(new List { $"{_settings.CallbackHost}/twilio/typing-{audioIndex}.mp3" }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); } } else From 91a221a9eb2ad08d88529d3c56e561b03e237dfd Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Thu, 29 Aug 2024 11:49:34 -0500 Subject: [PATCH 5/8] msg.Instruction?.ConversationEnd --- .../Controllers/TwilioVoiceController.cs | 14 +++++++------- .../Services/TwilioMessageQueueService.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index b0745b3d..4cb1fa9c 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -76,9 +76,8 @@ public class TwilioVoiceController : TwilioController } await messageQueue.EnqueueAsync(callerMessage); - int audioIndex = Random.Shared.Next(2, 5); response = new VoiceResponse() - .Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}&play=%23hold-on-{audioIndex}%7c%23typing-2"), HttpMethod.Post); + .Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post); } else { @@ -104,9 +103,7 @@ public class TwilioVoiceController : TwilioController VoiceResponse response; if (reply == null) { - var indication = string.IsNullOrEmpty(play) ? - await sessionManager.GetReplyIndicationAsync(conversationId, seqNum) : - play; + var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum); if (indication != null) { var speechPaths = new List(); @@ -131,8 +128,11 @@ public class TwilioVoiceController : TwilioController } else { - int audioIndex = Random.Shared.Next(1, 4); - response = twilio.ReturnInstructions(new List { $"{_settings.CallbackHost}/twilio/typing-{audioIndex}.mp3" }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); + response = twilio.ReturnInstructions(new List + { + $"twilio/hold-on-{Random.Shared.Next(1, 5)}.mp3", + $"twilio/typing-{Random.Shared.Next(2, 4)}.mp3" + }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); } } else diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs index 19e0ce68..3338c46d 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs @@ -82,7 +82,7 @@ namespace BotSharp.Plugin.Twilio.Services { reply = new AssistantMessage() { - ConversationEnd = msg.Instruction.ConversationEnd, + ConversationEnd = msg.Instruction?.ConversationEnd ?? false, Content = msg.Content, MessageId = msg.MessageId }; From e6f03fcdf4d9426c8b28b975d0935e68b0d6613d Mon Sep 17 00:00:00 2001 From: Bo Yin <103488@smsassist.com> Date: Thu, 29 Aug 2024 14:36:07 -0500 Subject: [PATCH 6/8] improve the workflow --- .../Controllers/TwilioVoiceController.cs | 27 +++++++++++++--- .../Services/ITwilioSessionManager.cs | 1 + .../Services/TwilioService.cs | 31 +++++++++++++++++-- .../Services/TwilioSessionManager.cs | 7 +++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index 4cb1fa9c..25b817d5 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -36,13 +36,13 @@ public class TwilioVoiceController : TwilioController string conversationId = $"TwilioVoice_{request.CallSid}"; var twilio = _services.GetRequiredService(); var url = $"twilio/voice/{conversationId}/receive/0?states={states}"; - var response = twilio.ReturnInstructions(new List { "twilio/welcome.mp3" }, url, true); + var response = twilio.ReturnNoninterruptedInstructions(new List { "twilio/welcome.mp3" }, url, true); return TwiML(response); } [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")] - public async Task ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request) + public async Task ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, [FromQuery] int attempts, VoiceRequest request) { var twilio = _services.GetRequiredService(); var messageQueue = _services.GetRequiredService(); @@ -81,7 +81,21 @@ public class TwilioVoiceController : TwilioController } else { - response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true); + if (attempts >= 3) + { + var speechPaths = new List(); + if (seqNum == 0) + { + speechPaths.Add("twilio/welcome.mp3"); + } + else + { + var lastRepy = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum - 1); + speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}"); + } + response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true); + } + response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}&attempts={++attempts}", true); } return TwiML(response); @@ -90,7 +104,7 @@ public class TwilioVoiceController : TwilioController [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")] public async Task ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, - [FromQuery] string states, [FromQuery] string play, VoiceRequest request) + [FromQuery] string states, VoiceRequest request) { var nextSeqNum = seqNum + 1; var sessionManager = _services.GetRequiredService(); @@ -107,6 +121,7 @@ public class TwilioVoiceController : TwilioController if (indication != null) { var speechPaths = new List(); + int segIndex = 0; foreach (var text in indication.Split('|')) { var seg = text.Trim(); @@ -119,12 +134,14 @@ public class TwilioVoiceController : TwilioController var textToSpeechService = CompletionProvider.GetTextToSpeech(_services, "openai", "tts-1"); var fileService = _services.GetRequiredService(); var data = await textToSpeechService.GenerateSpeechFromTextAsync(seg); - var fileName = $"indication_{seqNum}.mp3"; + var fileName = $"indication_{seqNum}_{segIndex}.mp3"; await fileService.SaveSpeechFileAsync(conversationId, fileName, data); speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}"); + segIndex++; } } response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); + await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum); } else { diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs index b1acd298..3651a6ec 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs @@ -11,5 +11,6 @@ namespace BotSharp.Plugin.Twilio.Services Task> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum); Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication); Task GetReplyIndicationAsync(string conversationId, int seqNum); + Task RemoveReplyIndicationAsync(string conversationId, int seqNum); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index 5ccdc7db..ca489682 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -76,8 +76,8 @@ public class TwilioService }, Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"), SpeechModel = Gather.SpeechModelEnum.PhoneCall, - SpeechTimeout = timeout > 0 ? timeout.ToString() : "3", - Timeout = timeout > 0 ? timeout : 3, + SpeechTimeout = timeout > 0 ? timeout.ToString() : "2", + Timeout = timeout > 0 ? timeout : 2, ActionOnEmptyResult = actionOnEmptyResult }; if (speechPaths != null && speechPaths.Any()) @@ -91,6 +91,33 @@ public class TwilioService return response; } + public VoiceResponse ReturnNoninterruptedInstructions(List speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 2) + { + var response = new VoiceResponse(); + if (speechPaths != null && speechPaths.Any()) + { + foreach (var speechPath in speechPaths) + { + response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}")); + } + } + var gather = new Gather() + { + Input = new List() + { + Gather.InputEnum.Speech, + Gather.InputEnum.Dtmf + }, + Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"), + SpeechModel = Gather.SpeechModelEnum.PhoneCall, + SpeechTimeout = timeout > 0 ? timeout.ToString() : "2", + Timeout = timeout > 0 ? timeout : 2, + ActionOnEmptyResult = actionOnEmptyResult + }; + response.Append(gather); + return response; + } + public VoiceResponse HangUp(string speechPath) { var response = new VoiceResponse(); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs index eae0b238..40924fba 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs @@ -59,5 +59,12 @@ namespace BotSharp.Plugin.Twilio.Services var key = $"{conversationId}:Indication:{seqNum}"; return await db.StringGetAsync(key); } + + public async Task RemoveReplyIndicationAsync(string conversationId, int seqNum) + { + var db = _redis.GetDatabase(); + var key = $"{conversationId}:Indication:{seqNum}"; + await db.KeyDeleteAsync(key); + } } } From 878ffde28145823d0bbcbfea22edd02cad1cd4ed Mon Sep 17 00:00:00 2001 From: Haiping Chen <101423@smsassist.com> Date: Thu, 29 Aug 2024 16:33:32 -0500 Subject: [PATCH 7/8] adjust two stage planner. --- .../Agents/Enums/AgentType.cs | 9 +- .../Agents/Enums/BuiltInAgentId.cs | 5 + .../BotSharp.Core/BotSharp.Core.csproj | 16 - .../TwoStagePlanner.FirstStage.cs | 4 +- .../TwoStagePlanner/TwoStagePlanner.cs | 38 +-- .../BotSharp.Core/Routing/RoutingPlugin.cs | 1 - .../planner_prompt.two_stage.2nd.task.liquid | 8 - .../planning.two_stage.primary_plan.liquid | 1 - .../BotSharp.Plugin.Planner.csproj | 28 ++ .../Functions/PrimaryStagePlanFn.cs | 22 +- .../Functions/SecondaryStagePlanFn.cs | 7 +- .../Functions/SummaryPlanFn.cs | 94 ++++++ .../Hooks/PlannerAgentHook.cs | 18 + .../BotSharp.Plugin.Planner/PlannerPlugin.cs | 11 +- .../TwoStaging/TwoStageTaskPlanner.cs | 310 +++++++++++++++++- .../agent.json | 17 + .../instructions/instruction.liquid | 3 + .../templates/two_stage.1st.plan.liquid} | 0 .../templates/two_stage.2nd.plan.liquid} | 0 .../templates/two_stage.summarize.liquid} | 0 .../functions/plan_primary_stage.json | 6 +- .../functions/plan_secondary_stage.json | 36 +- .../functions/plan_summary.json | 10 + .../templates/plan_secondary_stage.fn.liquid | 2 +- .../templates/plan_summary.fn.liquid | 1 + 25 files changed, 539 insertions(+), 108 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.task.liquid delete mode 100644 src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planning.two_stage.primary_plan.liquid create mode 100644 src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs create mode 100644 src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json create mode 100644 src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid rename src/{Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.1st.plan.liquid => Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid} (100%) rename src/{Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.plan.liquid => Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.2nd.plan.liquid} (100%) rename src/{Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.summarize.liquid => Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid} (100%) create mode 100644 src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_summary.json create mode 100644 src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_summary.fn.liquid diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs index 17689407..53d64a07 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs @@ -3,11 +3,16 @@ namespace BotSharp.Abstraction.Agents.Enums; public class AgentType { /// - /// Routing Agent + /// Routing agent /// public const string Routing = "routing"; - public const string Evaluating = "evaluating"; + /// + /// Planning agent + /// + public const string Planning = "plan"; + + public const string Evaluating = "evaluation"; /// /// Routable task agent with capability of interaction with external environment diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs index 58da1fd4..2c95818f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs @@ -31,4 +31,9 @@ public class BuiltInAgentId /// Used by knowledgebase plugin to acquire domain knowledge /// public const string Learner = "01acc3e5-0af7-49e6-ad7a-a760bd12dc40"; + + /// + /// Plan feasible implementation steps for complex problems + /// + public const string Planner = "282a7128-69a1-44b0-878c-a9159b88f3b9"; } diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 5787c463..ce4c82d2 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -56,7 +56,6 @@ - @@ -75,9 +74,6 @@ - - - @@ -119,15 +115,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -140,9 +127,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.FirstStage.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.FirstStage.cs index 932fb377..98efbacc 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.FirstStage.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.FirstStage.cs @@ -44,7 +44,7 @@ public partial class TwoStagePlanner private async Task GetFirstStagePlanPrompt(Agent router) { - var template = router.Templates.First(x => x.Name == "planner_prompt.two_stage.1st.plan").Content; + var template = router.Templates.First(x => x.Name == "two_stage.1st.plan").Content; var responseFormat = JsonSerializer.Serialize(new FirstStagePlan { Parameters = new JsonDocument[]{ JsonDocument.Parse("{}") }, @@ -69,7 +69,7 @@ public partial class TwoStagePlanner private string GetFirstStageNextPrompt(Agent router) { - var template = router.Templates.First(x => x.Name == "planner_prompt.first_stage.next").Content; + var template = router.Templates.First(x => x.Name == "first_stage.next").Content; var responseFormat = JsonSerializer.Serialize(new FirstStagePlan { }); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs index 6e6a4db5..8d65f566 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/TwoStagePlanner.cs @@ -1,9 +1,4 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Planning; -using System.IO; namespace BotSharp.Core.Routing.Planning; @@ -28,25 +23,9 @@ public partial class TwoStagePlanner : IRoutingPlaner public async Task GetNextInstruction(Agent router, string messageId, List dialogs) { - var tempDir = Path.Combine(Path.GetTempPath(), "botsharp", "cache"); if (_plan1st.IsNullOrEmpty() && _plan2nd.IsNullOrEmpty()) { - Directory.CreateDirectory(tempDir); - _md5 = Utilities.HashTextMd5($"{string.Join(".", dialogs.Where(x => x.Role == AgentRole.User))}{"botsharp"}"); - var filePath = Path.Combine(tempDir, $"{_md5}-1st.json"); - FirstStagePlan[] items = new FirstStagePlan[0]; - if (File.Exists(filePath)) - { - var cache = File.ReadAllText(filePath); - items = JsonSerializer.Deserialize(cache); - } - else - { - items = await GetFirstStagePlanAsync(router, messageId, dialogs); - - var cache = JsonSerializer.Serialize(items); - File.WriteAllText(filePath, cache); - } + FirstStagePlan[] items = await GetFirstStagePlanAsync(router, messageId, dialogs); foreach (var item in items) { @@ -61,20 +40,7 @@ public partial class TwoStagePlanner : IRoutingPlaner if (plan1.ContainMultipleSteps) { - var filePath = Path.Combine(tempDir, $"{_md5}-2nd-{plan1.Step}.json"); - SecondStagePlan[] items = new SecondStagePlan[0]; - if (File.Exists(filePath)) - { - var cache = File.ReadAllText(filePath); - items = JsonSerializer.Deserialize(cache); - } - else - { - items = await GetSecondStagePlanAsync(router, messageId, plan1, dialogs); - - var cache = JsonSerializer.Serialize(items); - File.WriteAllText(filePath, cache); - } + SecondStagePlan[] items = await GetSecondStagePlanAsync(router, messageId, plan1, dialogs); foreach (var item in items) { diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs index 33506d61..7a751057 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs @@ -38,6 +38,5 @@ public class RoutingPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.task.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.task.liquid deleted file mode 100644 index 0e833d53..00000000 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.task.liquid +++ /dev/null @@ -1,8 +0,0 @@ -{{ task_description }} - -{% if related_tables != empty -%} -Relevant tables: -{% for t in related_tables -%} -- {{ t }}{{ "\r\n" }} -{%- endfor %} -{%- endif %} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planning.two_stage.primary_plan.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planning.two_stage.primary_plan.liquid deleted file mode 100644 index 5f282702..00000000 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planning.two_stage.primary_plan.liquid +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj b/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj index 19813c95..63a66aac 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj +++ b/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj @@ -11,25 +11,53 @@ + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs index fa6db8c4..a12cafed 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs @@ -3,10 +3,8 @@ using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Templating; using System.Threading.Tasks; using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.MLTasks; using BotSharp.Core.Infrastructures; using BotSharp.Plugin.Planner.TwoStaging.Models; -using BotSharp.Abstraction.Knowledges; using Microsoft.Extensions.Logging; using BotSharp.Abstraction.Knowledges.Models; @@ -55,7 +53,7 @@ public class PrimaryStagePlanFn : IFunctionCallback var firstPlanningPrompt = await GetFirstStagePlanPrompt(task, message); var plannerAgent = new Agent { - Id = "", + Id = BuiltInAgentId.Planner, Name = "planning_1st", Instruction = firstPlanningPrompt, TemplateDict = new Dictionary(), @@ -64,7 +62,7 @@ public class PrimaryStagePlanFn : IFunctionCallback var response = await GetAIResponse(plannerAgent); message.Content = response.Content; - await fn.InvokeFunction("plan_secondary_stage", message); + /*await fn.InvokeFunction("plan_secondary_stage", message); var items = message.Content.JsonArrayContent(); //get all the related tables @@ -85,7 +83,7 @@ public class PrimaryStagePlanFn : IFunctionCallback _logger.LogInformation(summaryPlanningPrompt); plannerAgent = new Agent { - Id = "", + Id = BuiltInAgentId.Planner, Name = "planner_summary", Instruction = summaryPlanningPrompt, TemplateDict = new Dictionary(), @@ -95,19 +93,19 @@ public class PrimaryStagePlanFn : IFunctionCallback _logger.LogInformation(response_summary.Content); message.Content = response_summary.Content; - message.StopCompletion = true; + message.StopCompletion = true;*/ return true; } private async Task GetFirstStagePlanPrompt(PrimaryRequirementRequest task, RoleDialogModel message) { var agentService = _services.GetRequiredService(); - var aiAssistant = await agentService.GetAgent(BuiltInAgentId.AIAssistant); + var aiAssistant = await agentService.GetAgent(BuiltInAgentId.Planner); var render = _services.GetRequiredService(); - var template = aiAssistant.Templates.First(x => x.Name == "planner_prompt.two_stage.1st.plan").Content; + var template = aiAssistant.Templates.First(x => x.Name == "two_stage.1st.plan").Content; var responseFormat = JsonSerializer.Serialize(new FirstStagePlan { - Parameters = new JsonDocument[] { JsonDocument.Parse("{}") }, - Results = new string[] { "" } + Parameters = [JsonDocument.Parse("{}")], + Results = [""] }); return render.Render(template, new Dictionary @@ -126,8 +124,8 @@ public class PrimaryStagePlanFn : IFunctionCallback var template = aiAssistant.Templates.First(x => x.Name == "planner_prompt.two_stage.summarize").Content; var responseFormat = JsonSerializer.Serialize(new FirstStagePlan { - Parameters = new JsonDocument[] { JsonDocument.Parse("{}") }, - Results = new string[] { "" } + Parameters = [JsonDocument.Parse("{}")], + Results = [""] }); return render.Render(template, new Dictionary diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs index dd27c9fa..d6e02299 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs @@ -24,6 +24,7 @@ public class SecondaryStagePlanFn : IFunctionCallback _services = services; _logger = logger; } + public async Task Execute(RoleDialogModel message) { var fn = _services.GetRequiredService(); @@ -36,7 +37,7 @@ public class SecondaryStagePlanFn : IFunctionCallback var task_secondary = JsonSerializer.Deserialize(msg_secondary.FunctionArgs); var items = msg_secondary.Content.JsonArrayContent(); - msg_secondary.KnowledgeConfidence = 0.5f; + msg_secondary.KnowledgeConfidence = 0.5f; foreach (var item in items) { if (item.NeedAdditionalInformation) @@ -73,9 +74,9 @@ public class SecondaryStagePlanFn : IFunctionCallback private async Task GetSecondStagePlanPrompt(SecondaryBreakdownTask task, RoleDialogModel message) { var agentService = _services.GetRequiredService(); - var aiAssistant = await agentService.GetAgent(BuiltInAgentId.AIAssistant); + var planner = await agentService.GetAgent(message.CurrentAgentId); var render = _services.GetRequiredService(); - var template = aiAssistant.Templates.First(x => x.Name == "planner_prompt.two_stage.2nd.plan").Content; + var template = planner.Templates.First(x => x.Name == "two_stage.2nd.plan").Content; var responseFormat = JsonSerializer.Serialize(new SecondStagePlan { Tool = "tool name if task solution provided", diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs new file mode 100644 index 00000000..098edd53 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs @@ -0,0 +1,94 @@ +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Templating; +using System.Threading.Tasks; +using BotSharp.Core.Infrastructures; +using BotSharp.Plugin.Planner.TwoStaging.Models; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.Planner.Functions; + +public class SummaryPlanFn : IFunctionCallback +{ + public string Name => "plan_summary"; + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private object aiAssistant; + + public SummaryPlanFn(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Execute(RoleDialogModel message) + { + //debug + var state = _services.GetRequiredService(); + state.SetState("max_tokens", "4096"); + + var task = state.GetState("requirement_detail"); + + // summarize and generate query + var summaryPlanningPrompt = await GetPlanSummaryPrompt(task, message); + _logger.LogInformation(summaryPlanningPrompt); + var plannerAgent = new Agent + { + Id = BuiltInAgentId.Planner, + Name = "planner_summary", + Instruction = summaryPlanningPrompt, + TemplateDict = new Dictionary() + }; + var response_summary = await GetAIResponse(plannerAgent); + + message.Content = response_summary.Content; + message.StopCompletion = true; + + return true; + } + + private async Task GetPlanSummaryPrompt(string task, RoleDialogModel message) + { + // save to knowledge base + var agentService = _services.GetRequiredService(); + var aiAssistant = await agentService.GetAgent(message.CurrentAgentId); + var render = _services.GetRequiredService(); + var template = aiAssistant.Templates.First(x => x.Name == "two_stage.summarize").Content; + var responseFormat = JsonSerializer.Serialize(new FirstStagePlan + { + Parameters = [JsonDocument.Parse("{}")], + Results = [""] + }); + + return render.Render(template, new Dictionary + { + { "table_structure", message.SecondaryContent }, ////check + { "task_description", task}, + { "relevant_knowledges", message.Content }, + { "response_format", responseFormat } + }); + } + private async Task GetAIResponse(Agent plannerAgent) + { + var conv = _services.GetRequiredService(); + var wholeDialogs = conv.GetDialogHistory(); + //add "test" to wholeDialogs' last element + if(plannerAgent.Name == "planner_summary") + { + //add "test" to wholeDialogs' last element in a new paragraph + wholeDialogs.Last().Content += "\n\nIf the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id) instead of LAST_INSERT_ID function.\nFor example, you should use SET @id = select max(id) from table;"; + wholeDialogs.Last().Content += "\n\nTry if you can generate a single query to fulfill the needs"; + } + if (plannerAgent.Name == "planning_1st") + { + //add "test" to wholeDialogs' last element in a new paragraph + wholeDialogs.Last().Content += "\n\nYou must analyze the table description to infer the table relations."; + } + + var completion = CompletionProvider.GetChatCompletion(_services, + provider: plannerAgent.LlmConfig.Provider, + model: plannerAgent.LlmConfig.Model); + + return await completion.GetChatCompletions(plannerAgent, wholeDialogs); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs b/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs index 962c7104..1a85e1c0 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs @@ -52,6 +52,24 @@ public class PlannerAgentHook : AgentHookBase agent.Functions.Add(fn); } } + + (prompt, fn) = GetPromptAndFunction("plan_summary"); + if (fn != null) + { + if (!string.IsNullOrWhiteSpace(prompt)) + { + agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; + } + + if (agent.Functions == null) + { + agent.Functions = new List { fn }; + } + else + { + agent.Functions.Add(fn); + } + } } base.OnAgentLoaded(agent); diff --git a/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs b/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs index 3bc28c94..aba02745 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs @@ -1,14 +1,23 @@ +using BotSharp.Abstraction.Routing.Planning; +using BotSharp.Plugin.Planner.TwoStaging; + namespace BotSharp.Plugin.Planner; +/// +/// Plugin for AI Planning. +/// public class PlannerPlugin : IBotSharpPlugin { public string Id => "571f71fe-1583-46f2-b577-c8577a0a2903"; public string Name => "AI Planning Plugin"; public string Description => "Provide AI with different planning approaches to improve AI's ability to solve complex problems."; - public string IconUrl => "https://library.ucf.edu/wp-content/uploads/sites/5/2015/03/SC-Planning-Icon-300x290.png"; + public string IconUrl => "https://e7.pngegg.com/pngimages/775/350/png-clipart-action-plan-computer-icons-plan-miscellaneous-text-thumbnail.png"; + + public string[] AgentIds => [ BuiltInAgentId.Planner ]; public void RegisterDI(IServiceCollection services, IConfiguration config) { + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs index e94d641e..5f49d23b 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -1,11 +1,317 @@ +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Knowledges; +using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Planning; +using BotSharp.Abstraction.Templating; +using BotSharp.Core.Infrastructures; +using BotSharp.Core.Routing.Planning; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + namespace BotSharp.Plugin.Planner.TwoStaging; -public partial class TwoStageTaskPlanner : ITaskPlanner +public partial class TwoStageTaskPlanner : IRoutingPlaner { private readonly IServiceProvider _services; + private readonly ILogger _logger; + public int MaxLoopCount => 100; + private bool _isTaskCompleted; - public TwoStageTaskPlanner(IServiceProvider services) + private Queue _plan1st = new Queue(); + private Queue _plan2nd = new Queue(); + + private List _executionContext = new List(); + + public TwoStageTaskPlanner(IServiceProvider services, ILogger logger) { _services = services; + _logger = logger; + } + + public async Task GetNextInstruction(Agent router, string messageId, List dialogs) + { + // push agent to routing context + var routing = _services.GetRequiredService(); + routing.Context.Push(BuiltInAgentId.Planner, "Make plan in TwoStage planner"); + + return new FunctionCallFromLlm + { + AgentName = "Planner", + UserGoal = "", + Response = "", + Function = "route_to_agent" + }; + + /*FirstStagePlan[] items = await GetFirstStagePlanAsync(router, messageId, dialogs); + + foreach (var item in items) + { + _plan1st.Enqueue(item); + }; + + // Get Second Stage Plan + if (_plan2nd.IsNullOrEmpty()) + { + var plan1 = _plan1st.Dequeue(); + + if (plan1.ContainMultipleSteps) + { + SecondStagePlan[] items = await GetSecondStagePlanAsync(router, messageId, plan1, dialogs); + + foreach (var item in items) + { + _plan2nd.Enqueue(item); + } + } + else + { + _plan2nd.Enqueue(new SecondStagePlan + { + Description = plan1.Task, + Tables = plan1.Tables, + Parameters = plan1.Parameters, + Results = plan1.Results, + }); + } + } + + var plan2 = _plan2nd.Dequeue(); + + var secondStagePrompt = GetSecondStageTaskPrompt(router, plan2); + var inst = new FunctionCallFromLlm + { + AgentName = "SQL Driver", + Response = secondStagePrompt, + Function = "route_to_agent" + }; + + inst.HandleDialogsByPlanner = true; + _isTaskCompleted = _plan1st.IsNullOrEmpty() && _plan2nd.IsNullOrEmpty(); + + return inst;*/ + } + + public List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + var question = inst.Response; + if (_executionContext.Count > 0) + { + var content = GetContext(); + question = $"CONTEXT:\r\n{content}\r\n" + inst.Response; + } + else + { + question = $"CONTEXT:\r\n{question}"; + } + + var taskAgentDialogs = new List + { + new RoleDialogModel(AgentRole.User, question) + { + MessageId = message.MessageId, + } + }; + + return taskAgentDialogs; + } + + public bool AfterHandleContext(List dialogs, List taskAgentDialogs) + { + dialogs.AddRange(taskAgentDialogs.Skip(1)); + + // Keep execution context + _executionContext.Add(taskAgentDialogs.Last().Content); + + return true; + } + + public async Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + dialogs.Add(new RoleDialogModel(AgentRole.User, inst.Response) + { + MessageId = message.MessageId, + CurrentAgentId = router.Id + }); + return true; + } + + public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + var context = _services.GetRequiredService(); + + if (message.StopCompletion || _isTaskCompleted) + { + context.Empty(reason: $"Agent queue is cleared by {nameof(TwoStagePlanner)}"); + return false; + } + + var routing = _services.GetRequiredService(); + routing.ResetRecursiveCounter(); + return true; + } + + public string GetContext() + { + var content = ""; + foreach (var c in _executionContext) + { + content += $"* {c}\r\n"; + } + return content; + } + + private async Task GetFirstStagePlanAsync(Agent router, string messageId, List dialogs) + { + /*var fn = _services.GetRequiredService(); + await fn.InvokeFunction("plan_primary_stage", message); + var items = message.Content.JsonArrayContent();*/ + + var firstStagePlanPrompt = await GetFirstStagePlanPrompt(router); + + var plan = new FirstStagePlan[0]; + + var llmProviderService = _services.GetRequiredService(); + var provider = router.LlmConfig.Provider ?? "openai"; + var model = llmProviderService.GetProviderModel(provider, router.LlmConfig.Model ?? "gpt-4o"); + + // chat completion + var completion = CompletionProvider.GetChatCompletion(_services, + provider: provider, + model: model.Name); + + string text = string.Empty; + + try + { + var response = await completion.GetChatCompletions(new Agent + { + Id = router.Id, + Name = nameof(TwoStagePlanner), + Instruction = firstStagePlanPrompt + }, dialogs); + + text = response.Content; + plan = response.Content.JsonArrayContent(); + } + catch (Exception ex) + { + _logger.LogError($"{ex.Message}: {text}"); + } + + return plan; + } + + private async Task GetFirstStagePlanPrompt(Agent router) + { + var template = router.Templates.First(x => x.Name == "two_stage.1st.plan").Content; + var responseFormat = JsonSerializer.Serialize(new FirstStagePlan + { + Parameters = new JsonDocument[] { JsonDocument.Parse("{}") }, + Results = new string[] { "" } + }); + + var relevantKnowledges = new List(); + var hooks = _services.GetServices(); + foreach (var hook in hooks) + { + var k = await hook.GetRelevantKnowledges(); + relevantKnowledges.AddRange(k); + } + + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { "response_format", responseFormat }, + { "relevant_knowledges", relevantKnowledges.ToArray() } + }); + } + + private string GetFirstStageNextPrompt(Agent router) + { + var template = router.Templates.First(x => x.Name == "first_stage.next").Content; + var responseFormat = JsonSerializer.Serialize(new FirstStagePlan + { + }); + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { "response_format", responseFormat }, + }); + } + + private async Task GetSecondStagePlanAsync(Agent router, string messageId, FirstStagePlan plan1st, List dialogs) + { + var secondStagePrompt = GetSecondStagePlanPrompt(router, plan1st); + var firstStageSystemPrompt = await GetFirstStagePlanPrompt(router); + + var plan = new SecondStagePlan[0]; + + var llmProviderService = _services.GetRequiredService(); + var model = llmProviderService.GetProviderModel("azure-openai", "gpt-4"); + + // chat completion + var completion = CompletionProvider.GetChatCompletion(_services, + provider: "azure-openai", + model: model.Name); + + string text = string.Empty; + + var conversations = dialogs.Where(x => x.Role != AgentRole.Function).ToList(); + conversations.Add(new RoleDialogModel(AgentRole.User, secondStagePrompt) + { + CurrentAgentId = router.Id, + MessageId = messageId, + }); + + try + { + var response = await completion.GetChatCompletions(new Agent + { + Id = router.Id, + Name = nameof(TwoStagePlanner), + Instruction = firstStageSystemPrompt + }, conversations); + + text = response.Content; + plan = response.Content.JsonArrayContent(); + } + catch (Exception ex) + { + _logger.LogError($"{ex.Message}: {text}"); + } + + return plan; + } + + private string GetSecondStageTaskPrompt(Agent router, SecondStagePlan plan) + { + var template = router.Templates.First(x => x.Name == "planner_prompt.two_stage.2nd.task").Content; + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { "task_description", plan.Description }, + { "related_tables", plan.Tables }, + { "input_arguments", JsonSerializer.Serialize(plan.Parameters) }, + { "output_results", JsonSerializer.Serialize(plan.Results) }, + }); + } + + private string GetSecondStagePlanPrompt(Agent router, FirstStagePlan plan) + { + var template = router.Templates.First(x => x.Name == "planner_prompt.two_stage.2nd.plan").Content; + var responseFormat = JsonSerializer.Serialize(new SecondStagePlan + { + Tool = "tool name if task solution provided", + Parameters = new JsonDocument[] { JsonDocument.Parse("{}") }, + Results = new string[] { "" } + }); + var context = GetContext(); + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { "task_description", plan.Task }, + { "response_format", responseFormat } + }); } } diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json new file mode 100644 index 00000000..f689f24f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json @@ -0,0 +1,17 @@ +{ + "id": "282a7128-69a1-44b0-878c-a9159b88f3b9", + "name": "Planner", + "description": "Plan feasible implementation steps for complex problems", + "type": "task", + "createdDateTime": "2023-08-27T10:39:00Z", + "updatedDateTime": "2023-08-27T14:39:00Z", + "iconUrl": "https://e7.pngegg.com/pngimages/775/350/png-clipart-action-plan-computer-icons-plan-miscellaneous-text-thumbnail.png", + "disabled": false, + "isPublic": true, + "profiles": [ "tool" ], + "utilities": [ "two-stage-planner" ], + "llmConfig": { + "provider": "openai", + "model": "gpt-4o-mini" + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid new file mode 100644 index 00000000..f40fa6c8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid @@ -0,0 +1,3 @@ +Use the TwoStagePlanner approach to plan the overall implementation steps, call plan_primary_stage. +If need_additional_information is true, call plan_secondary_stage for the specific primary stage. +Call plan_summary to summarize the final planning steps. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.1st.plan.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.1st.plan.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.plan.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.2nd.plan.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.2nd.plan.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.2nd.plan.liquid diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.summarize.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.two_stage.summarize.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_primary_stage.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_primary_stage.json index b27ee293..fcd96595 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_primary_stage.json +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_primary_stage.json @@ -8,15 +8,11 @@ "type": "string", "description": "User original requirements in detail, don't miss any information especially for those line items, values and numbers." }, - "has_knowledge_reference": { - "type": "boolean", - "description": "If there is knowledge retrieved from memory" - }, "question": { "type": "string", "description": "Question convert from requirement and reference tables for knowledge search. The question should contain all the detailed information in the requirement" } }, - "required": [ "requirement_detail", "has_knowledge_reference", "question" ] + "required": [ "requirement_detail", "question" ] } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_secondary_stage.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_secondary_stage.json index 26fbd3ae..b3770e9a 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_secondary_stage.json +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_secondary_stage.json @@ -1,18 +1,18 @@ -//{ -// "name": "plan_secondary_stage", -// "description": "Based on the main tasks of the first phase, plan the implementation steps of the second phase.", -// "parameters": { -// "type": "object", -// "properties": { -// "task_description": { -// "type": "string", -// "description": "task description from primary steps" -// }, -// "solution_search_question": { -// "type": "string", -// "description": "Provide solution query text" -// } -// }, -// "required": [ "task_description", "solution_search_question" ] -// } -//} \ No newline at end of file +{ + "name": "plan_secondary_stage", + "description": "Based on the primary stage planning, make more detail steps of the second stage if the primary stage needs more information.", + "parameters": { + "type": "object", + "properties": { + "task_description": { + "type": "string", + "description": "task description from primary steps" + }, + "solution_search_question": { + "type": "string", + "description": "Provide solution query text" + } + }, + "required": [ "task_description", "solution_search_question" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_summary.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_summary.json new file mode 100644 index 00000000..0345b744 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/plan_summary.json @@ -0,0 +1,10 @@ +{ + "name": "plan_summary", + "description": "Based on the planning steps, summarize the planning steps and output final steps.", + "parameters": { + "type": "object", + "properties": { + }, + "required": [] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_secondary_stage.fn.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_secondary_stage.fn.liquid index 5dd51408..726fe329 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_secondary_stage.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_secondary_stage.fn.liquid @@ -1 +1 @@ -For every primary step, you have to call plan_secondary_stage to plan the detail steps to complete the primary step. \ No newline at end of file +For every primary step, if need_additional_information is true, you have to call plan_secondary_stage to plan the detail steps to complete the primary step. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_summary.fn.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_summary.fn.liquid new file mode 100644 index 00000000..9033f34a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_summary.fn.liquid @@ -0,0 +1 @@ +Remove the unnecessary information, and output the final planning steps. \ No newline at end of file From a7fbc8fdf97a4b3f081e1ab1ab633e51fd26983b Mon Sep 17 00:00:00 2001 From: Haiping Chen <101423@smsassist.com> Date: Thu, 29 Aug 2024 17:32:39 -0500 Subject: [PATCH 8/8] AgentExecuting --- .../TwoStaging/TwoStageTaskPlanner.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs index 5f49d23b..be627c75 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -128,11 +128,6 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner public async Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) { - dialogs.Add(new RoleDialogModel(AgentRole.User, inst.Response) - { - MessageId = message.MessageId, - CurrentAgentId = router.Id - }); return true; }