From 33df282add01b30ed0dca985d5f65109a24d0472 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Fri, 22 Mar 2024 14:06:20 -0500 Subject: [PATCH] task completed hook. --- .../Conversations/ConversationHookBase.cs | 2 +- .../Conversations/IConversationHook.cs | 2 +- .../Functions/Models/FunctionCallFromLlm.cs | 2 +- .../Routing/Models/RoutingArgs.cs | 11 +++- .../BotSharp.Core/BotSharp.Core.csproj | 2 +- .../ConversationService.SendMessage.cs | 11 +++- .../Routing/Functions/RouteToAgentFn.cs | 2 +- .../Handlers/ConversationEndRoutingHandler.cs | 7 +- .../Handlers/ResponseToUserRoutingHandler.cs | 2 - .../RetrieveDataFromAgentRoutingHandler.cs | 3 - .../Handlers/TaskCompletedRoutingHandler.cs | 65 +++++++++++++++++++ .../Routing/Handlers/TaskEndRoutingHandler.cs | 41 ------------ .../Routing/Planning/SequentialPlanner.cs | 4 +- .../TwoStagePlanner/FirstStagePlan.cs | 3 + .../BotSharp.Core/Routing/RoutingContext.cs | 2 +- .../BotSharp.Core/Routing/RoutingService.cs | 2 +- .../instruction.liquid | 2 + .../templates/planner_prompt.naive.liquid | 2 +- .../Hooks/StreamingLogHook.cs | 33 ++++++++++ .../PlaywrightDriver/PlaywrightInstance.cs | 5 +- .../Hooks/PizzaTypeConversationHook.cs | 5 ++ 21 files changed, 145 insertions(+), 63 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index 4cf63cdf..5f0489ac 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -44,7 +44,7 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnConversationEnding(RoleDialogModel message) => Task.CompletedTask; - public virtual Task OnCurrentTaskEnding(RoleDialogModel message) + public virtual Task OnTaskCompleted(RoleDialogModel message) => Task.CompletedTask; public virtual Task OnHumanInterventionNeeded(RoleDialogModel message) diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index 1143c492..7d943754 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -70,7 +70,7 @@ public interface IConversationHook /// /// /// - Task OnCurrentTaskEnding(RoleDialogModel message); + Task OnTaskCompleted(RoleDialogModel message); /// /// LLM detected the whole conversation is going to be end. diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs index 26d9ecef..fefade10 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -35,7 +35,7 @@ public class FunctionCallFromLlm : RoutingArgs public override string ToString() { - var route = string.IsNullOrEmpty(AgentName) ? "" : $""; + var route = string.IsNullOrEmpty(AgentName) ? "" : $""; if (string.IsNullOrEmpty(Response)) { diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs index ebeac256..82de1a81 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs @@ -10,7 +10,14 @@ public class RoutingArgs /// [JsonPropertyName("next_action_reason")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Reason { get; set; } = string.Empty; + public string? NextActionReason { get; set; } + + [JsonPropertyName("reason")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Reason { get; set; } + + [JsonPropertyName("conversation_end")] + public bool ConversationEnd { get; set; } /// /// The content of replying to user @@ -39,7 +46,7 @@ public class RoutingArgs public override string ToString() { - var route = string.IsNullOrEmpty(AgentName) ? "" : $""; + var route = string.IsNullOrEmpty(AgentName) ? "" : $""; if (string.IsNullOrEmpty(Response)) { diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 48fd25df..da47ccb5 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -134,7 +134,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index e420152d..16f93e32 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -153,7 +153,16 @@ public partial class ConversationService if (response.Instruction != null) { var conversation = _services.GetRequiredService(); - var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.Reason); + var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.NextActionReason); + + // Emit conversation ending hook + if (response.Instruction.ConversationEnd) + { + foreach (var hook in hooks) + { + await hook.OnConversationEnding(response); + } + } } } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs index d2f478e8..6eaaef3a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -57,7 +57,7 @@ public partial class RouteToAgentFn : IFunctionCallback // Push next action agent if (!string.IsNullOrEmpty(args.AgentName) && args.AgentName.Length < 32) { - _context.Push(args.AgentName, args.Reason); + _context.Push(args.AgentName, args.NextActionReason); states.SetState("next_action_agent", args.AgentName, isNeedVersion: true); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs index 5a7b7c4b..bbd4fbb1 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing.Handlers; @@ -16,12 +14,15 @@ public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler new ParameterPropertyDef("response", "response content to user") }; + public List Planers => new List + { + }; + public ConversationEndRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) : base(services, logger, settings) { } - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) { var response = new RoleDialogModel(AgentRole.Assistant, inst.Response) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs index 1d12d4b5..5283b48d 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing.Handlers; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs index 24a048ae..ad4d3cab 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs @@ -1,6 +1,3 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; using BotSharp.Core.Routing.Planning; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs new file mode 100644 index 00000000..bb553266 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskCompletedRoutingHandler.cs @@ -0,0 +1,65 @@ +using BotSharp.Abstraction.Routing.Settings; +using BotSharp.Core.Routing.Planning; + +namespace BotSharp.Core.Routing.Handlers; + +public class TaskCompletedRoutingHandler : RoutingHandlerBase, IRoutingHandler +{ + public string Name => "task_completed"; + + public string Description => "User task is completed."; + + public List Parameters => new List + { + new ParameterPropertyDef("reason", "why the task is completed") + { + Required = true + }, + new ParameterPropertyDef("response", "polite response when the task is completed") + { + Required = true + }, + new ParameterPropertyDef("conversation_end", "whether to end this conversation, true or false") + { + Required = true, + Type = "boolean" + }, + new ParameterPropertyDef("abandoned_arguments", "the arguments next task can't reuse") + }; + + public List Planers => new List + { + nameof(NaivePlanner), + nameof(HFPlanner) + }; + + public TaskCompletedRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) + { + var response = new RoleDialogModel(AgentRole.Assistant, inst.Response) + { + CurrentAgentId = message.CurrentAgentId, + MessageId = message.MessageId, + StopCompletion = true, + FunctionName = inst.Function, + Instruction = inst, + }; + + _dialogs.Add(response); + + var hooks = _services.GetServices() + .OrderBy(x => x.Priority) + .ToList(); + + foreach (var hook in hooks) + { + await hook.OnTaskCompleted(response); + } + + return true; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs deleted file mode 100644 index 5e431f84..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Planning; - -namespace BotSharp.Core.Routing.Handlers; - -public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler -{ - public string Name => "task_end"; - - public string Description => "Call this function when current task is completed."; - - public List Parameters => new List - { - new ParameterPropertyDef("abandoned_arguments", "the arguments next task can't reuse") - }; - - public List Planers => new List - { - nameof(HFPlanner) - }; - - public TaskEndRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); - - Task.WaitAll(hooks - .Select(h => h.OnCurrentTaskEnding(message)) - .ToArray()); - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs index ff094770..b6f613e0 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs @@ -31,7 +31,7 @@ public class SequentialPlanner : IPlaner if (decomposation.TotalRemainingSteps > 0 && _lastInst != null) { _lastInst.Response = decomposation.Description; - _lastInst.Reason = $"Having {decomposation.TotalRemainingSteps} steps left."; + _lastInst.NextActionReason = $"Having {decomposation.TotalRemainingSteps} steps left."; return _lastInst; } else if (decomposation.TotalRemainingSteps == 0 || decomposation.ShouldStop) @@ -102,7 +102,7 @@ public class SequentialPlanner : IPlaner if (decomposation.TotalRemainingSteps > 0) { inst.Response = decomposation.Description; - inst.Reason = $"{decomposation.TotalRemainingSteps} steps left."; + inst.NextActionReason = $"{decomposation.TotalRemainingSteps} steps left."; inst.HandleDialogsByPlanner = true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlan.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlan.cs index 6b4bb672..76e697a5 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlan.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlan.cs @@ -19,6 +19,9 @@ public class FirstStagePlan [JsonPropertyName("related_tables")] public string[] Tables { get; set; } = new string[0]; + [JsonPropertyName("related_urls")] + public string[] Urls { get; set; } = new string[0]; + [JsonPropertyName("input_args")] public JsonDocument[] Parameters { get; set; } = new JsonDocument[0]; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 2b2d40a4..87e55840 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -122,7 +122,7 @@ public class RoutingContext : IRoutingContext { Function = "route_to_agent", AgentName = agent.Name, - Reason = $"User manually route to agent {agent.Name}" + NextActionReason = $"User manually route to agent {agent.Name}" }) }; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 8e2560bb..622926e7 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -54,7 +54,7 @@ public partial class RoutingService : IRoutingService { Function = "route_to_agent", Question = message.Content, - Reason = message.Content, + NextActionReason = message.Content, AgentName = agent.Name, OriginalAgent = agent.Name, ExecutingDirectly = true diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid index 990052cf..d7d0f359 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid @@ -7,10 +7,12 @@ You're {{router.name}} ({{router.description}}). Follow these steps to handle us 6. Please do not make up any parameters when there is no exact information available, leave it blank. 7. Response must be in JSON format. +{% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS] {% for requirement in routing_requirements %} # {{ requirement }} {% endfor %} +{% endif %} [FUNCTIONS] {% for handler in routing_handlers %} diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid index 2bf2698c..80aba1b9 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid @@ -3,5 +3,5 @@ Route to the appropriate agent last handled agent based on the context. {% if expected_next_action_agent != empty -%} Expected next action agent is {{ expected_next_action_agent }}. {%- endif %} -Try to keep the User Goal Agent be consistent as previous goal agent. +If user completes the task, use function task_completed. If user wants to speak to customer service, use function human_intervention_needed. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs index bc90877a..757b31a3 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/StreamingLogHook.cs @@ -41,6 +41,7 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR _routingCtx = routingCtx; } + #region IConversationHook public override async Task OnMessageReceived(RoleDialogModel message) { var conversationId = _state.GetConversationId(); @@ -156,6 +157,38 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR } } + public override async Task OnTaskCompleted(RoleDialogModel message) + { + var conversationId = _state.GetConversationId(); + var log = $"{message.Content}"; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); + + var input = new ContentLogInputModel(conversationId, message) + { + Name = agent.Name, + Source = ContentLogSource.FunctionCall, + Log = log + }; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + } + + public override async Task OnConversationEnding(RoleDialogModel message) + { + var conversationId = _state.GetConversationId(); + var log = $"Conversation ended"; + var agent = await _agentService.LoadAgent(message.CurrentAgentId); + + var input = new ContentLogInputModel(conversationId, message) + { + Name = agent?.Name ?? "System", + Source = ContentLogSource.FunctionCall, + Log = log + }; + await _chatHub.Clients.User(_user.Id).SendAsync("OnConversationContentLogGenerated", BuildContentLog(input)); + } + + #endregion + #region IRoutingHook public async Task OnAgentEnqueued(string agentId, string preAgentId, string? reason = null) { diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs index 84e32538..fdbc29cb 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightInstance.cs @@ -26,8 +26,11 @@ public class PlaywrightInstance : IDisposable { if (_contexts.ContainsKey(id)) return; - +#if DEBUG string tempFolderPath = $"{Path.GetTempPath()}\\playwright"; +#else + string tempFolderPath = $"{Path.GetTempPath()}\\playwright\\{id}"; +#endif _contexts[id] = await _playwright.Chromium.LaunchPersistentContextAsync(tempFolderPath, new BrowserTypeLaunchPersistentContextOptions { #if DEBUG diff --git a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs index 558fa047..6fb34772 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs @@ -13,4 +13,9 @@ public class PizzaTypeConversationHook : ConversationHookBase } return; } + + public override Task OnTaskCompleted(RoleDialogModel message) + { + return base.OnTaskCompleted(message); + } }