From e1a948c9c71565e2f03dc2d48eb617f61eaf53f0 Mon Sep 17 00:00:00 2001 From: Haiping Chen <101423@smsassist.com> Date: Fri, 26 Jan 2024 16:23:10 -0600 Subject: [PATCH] Allow task agent to fallback to predefined router. --- .../Functions/Models/FunctionParametersDef.cs | 2 +- .../Routing/Enums/RuleType.cs | 14 ++++++ .../Routing/IRoutingService.cs | 14 +++++- .../Routing/Models/RoutingContext.cs | 13 ++++++ .../Routing/Models/RoutingRule.cs | 7 ++- .../Routing/Functions/FallbackToRouterFn.cs | 44 +++++++++++++++++++ .../Routing/Functions/RouteToAgentFn.cs | 2 +- .../Routing/Hooks/RoutingAgentHook.cs | 41 +++++++++++++++-- .../Routing/RoutingService.InvokeAgent.cs | 11 ++--- .../BotSharp.Core/Routing/RoutingService.cs | 6 +-- .../instruction.liquid | 3 +- .../templates/planner_prompt.hf.liquid | 2 +- .../templates/planner_prompt.naive.liquid | 1 - .../planner_prompt.sequential.liquid | 3 +- .../Functions/InputUserTextFn.cs | 1 + 15 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs create mode 100644 src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs index d4e69e2a7..975a17d63 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionParametersDef.cs @@ -5,7 +5,7 @@ namespace BotSharp.Abstraction.Functions.Models; public class FunctionParametersDef { [JsonPropertyName("type")] - public string Type { get; set; } = "string"; + public string Type { get; set; } = "object"; /// /// ParameterPropertyDef diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs new file mode 100644 index 000000000..8b910f252 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Abstraction.Routing.Enums; + +public class RuleType +{ + /// + /// Fallback to redirect agent + /// + public const string Fallback = "fallback"; + + /// + /// Redirect to other agent if data validation failed + /// + public const string DataValidation = "data-validation"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index f6a14ecbd..8abcc9cae 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -13,8 +13,20 @@ public interface IRoutingService /// RoutableAgent[] GetRoutableAgents(List profiles); - RoutingRule[] GetRulesByName(string name); + /// + /// Get rules by agent name + /// + /// agent name + /// + RoutingRule[] GetRulesByAgentName(string name); + + /// + /// Get rules by agent id + /// + /// agent id + /// RoutingRule[] GetRulesByAgentId(string id); + List GetHandlers(); void ResetRecursiveCounter(); Task InvokeAgent(string agentId, List dialogs); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs index 085053e93..d09658d80 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs @@ -68,6 +68,19 @@ public void Pop() _stack.Pop(); } + public void Replace(string agentId) + { + if (_stack.Count == 0) + { + _stack.Push(agentId); + } + else if (_stack.Peek() != agentId) + { + _stack.Pop(); + _stack.Push(agentId); + } + } + public void Empty() { _stack.Clear(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs index 88835216b..b652ced17 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRule.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Routing.Enums; + namespace BotSharp.Abstraction.Routing.Models; public class RoutingRule @@ -8,12 +10,15 @@ public class RoutingRule [JsonIgnore] public string AgentName { get; set; } + public string Type { get; set; } = RuleType.DataValidation; + public string Field { get; set; } public string Description { get; set; } + /// /// Field type: string, number, object /// - public string Type { get; set; } = "string"; + public string FieldType { get; set; } = "string"; public bool Required { get; set; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs new file mode 100644 index 000000000..d6fb3dcb2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/FallbackToRouterFn.cs @@ -0,0 +1,44 @@ +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Routing.Models; +using BotSharp.Abstraction.Routing; + +namespace BotSharp.Core.Routing.Functions; + +public class FallbackToRouterFn : IFunctionCallback +{ + public string Name => "fallback_to_router"; + private readonly IServiceProvider _services; + public FallbackToRouterFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var agentService = _services.GetRequiredService(); + var agents = await agentService.GetAgents(new AgentFilter + { + AgentName = args.AgentName + }); + var targetAgent = agents.Items.FirstOrDefault(); + if (targetAgent == null) + { + message.Content = $"Can't find routing agent {args.AgentName}"; + return false; + } + + var routing = _services.GetRequiredService(); + routing.Replace(targetAgent.Id); + + var router = _services.GetRequiredService(); + message.CurrentAgentId = targetAgent.Id; + var response = await router.InstructLoop(message); + + message.Content = response.Content; + message.StopCompletion = true; + + return true; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs index a197b9bae..26f14f455 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -91,7 +91,7 @@ private bool HasMissingRequiredField(RoleDialogModel message, out string agentId var args = JsonSerializer.Deserialize(message.FunctionArgs); var routing = _services.GetRequiredService(); - var routingRules = routing.GetRulesByName(args.AgentName); + var routingRules = routing.GetRulesByAgentName(args.AgentName); if (routingRules == null || !routingRules.Any()) { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index bd11a5aa8..314941c03 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -1,6 +1,8 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; +using System.Diagnostics.Metrics; namespace BotSharp.Core.Routing.Hooks; @@ -33,11 +35,42 @@ public override bool OnInstructionLoaded(string template, Dictionary functions) { - /*functions.Add(new FunctionDef + if (_agent.Type == AgentType.Task) { - Name = "fallback_to_router", - Description = "If the user's request is beyond your capabilities, you can call this function for help." - });*/ + // check if enabled the routing rule + var routing = _services.GetRequiredService(); + var rule = routing.GetRulesByAgentId(_agent.Id) + .FirstOrDefault(x => x.Type == RuleType.Fallback); + if (rule != null) + { + var agentService = _services.GetRequiredService(); + var redirectAgent = agentService.GetAgent(rule.RedirectTo).Result; + + var json = JsonSerializer.Serialize(new + { + user_goal_agent = new + { + type = "string", + description = $"the fixed value is: {_agent.Name}" + }, + next_action_agent = new + { + type = "string", + description = $"the fixed value is: {redirectAgent.Name}" + } + }); + functions.Add(new FunctionDef + { + Name = "fallback_to_router", + Description = $"If the user's request is beyond your capabilities, you can call this function to handle by other agent ({redirectAgent.Name}).", + Parameters = + { + Properties = JsonSerializer.Deserialize(json) + } + }); + } + } + return base.OnFunctionsLoaded(functions); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index ccdee8d5c..1c7789bdf 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; @@ -70,7 +71,7 @@ private async Task InvokeFunction(RoleDialogModel message, List(); - + // Find response template var templateService = _services.GetRequiredService(); var responseTemplate = await templateService.RenderFunctionResponse(message.CurrentAgentId, message); @@ -83,8 +84,8 @@ private async Task InvokeFunction(RoleDialogModel message, List InvokeFunction(RoleDialogModel message, List profiles) Profiles = x.Profiles, RequiredFields = x.RoutingRules .Where(p => p.Required) - .Select(p => new ParameterPropertyDef(p.Field, p.Description, type: p.Type) + .Select(p => new ParameterPropertyDef(p.Field, p.Description, type: p.FieldType) { Required = p.Required }).ToList(), OptionalFields = x.RoutingRules .Where(p => !p.Required) - .Select(p => new ParameterPropertyDef(p.Field, p.Description, type: p.Type) + .Select(p => new ParameterPropertyDef(p.Field, p.Description, type: p.FieldType) { Required = p.Required }).ToList() @@ -202,7 +202,7 @@ public RoutableAgent[] GetRoutableAgents(List profiles) return routableAgents; } - public RoutingRule[] GetRulesByName(string name) + public RoutingRule[] GetRulesByAgentName(string name) { return GetRoutingRecords() .Where(x => x.AgentName.ToLower() == name.ToLower()) 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 eca5ac785..9147979f6 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 @@ -4,6 +4,7 @@ You're {{router.name}} ({{router.description}}). Follow these steps to handle us 3. Determine which agent is suitable to handle this conversation. 4. Re-think on whether the function you chose matches the reason. 5. For agent required arguments, leave it as blank object if user doesn't provide it. +6. Response must be in JSON format. [FUNCTIONS] {% for handler in routing_handlers %} @@ -36,4 +37,4 @@ Optional args: {% endfor %} [CONVERSATION] -{{ conversation }} \ No newline at end of file +{{ conversation }} diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.hf.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.hf.liquid index 158bf9659..20874a609 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.hf.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.hf.liquid @@ -1 +1 @@ -Break down the user’s most recent needs and figure out the next steps. Response must be in appropriate JSON format. \ No newline at end of file +Break down the user’s most recent needs and figure out the next steps. \ No newline at end of file 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 2f74b54b8..449bd4d7b 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 @@ -1,4 +1,3 @@ What is the next step based on the CONVERSATION? -Response must be in required JSON format without any other contents. Route to the Agent that last handled the conversation if necessary. If user wants to speak to customer service, use function human_intervention_needed. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.liquid index 33f8db9f6..a1025e503 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.liquid @@ -1,3 +1,2 @@ In order to execute the instructions listed by the user in the order specified by the user. -What is the next step based on the CONVERSATION? -Response must be in required JSON format. \ No newline at end of file +What is the next step based on the CONVERSATION? \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs index b5052965d..856203ed8 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs @@ -27,6 +27,7 @@ public async Task Execute(RoleDialogModel message) await _driver.InputUserText(agent, args, message.MessageId); message.Content = $"Input text \"{args.InputText}\" successfully."; + return true; } }