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 d4e69e2a..975a17d6 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 00000000..8b910f25 --- /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 f6a14ecb..8abcc9ca 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 085053e9..d09658d8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingContext.cs @@ -68,6 +68,19 @@ public class RoutingContext _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 88835216..b652ced1 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 00000000..d6fb3dcb --- /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 a197b9ba..26f14f45 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -91,7 +91,7 @@ public class RouteToAgentFn : IFunctionCallback 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 bd11a5aa..314941c0 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 class RoutingAgentHook : AgentHookBase public override bool OnFunctionsLoaded(List 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 ccdee8d5..1c7789bd 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 @@ public partial class RoutingService else if (!message.StopCompletion) { var routing = _services.GetRequiredService(); - + // Find response template var templateService = _services.GetRequiredService(); var responseTemplate = await templateService.RenderFunctionResponse(message.CurrentAgentId, message); @@ -83,8 +84,8 @@ public partial class RoutingService else { // Save to memory dialogs - dialogs.Add(RoleDialogModel.From(message, - role: AgentRole.Function, + dialogs.Add(RoleDialogModel.From(message, + role: AgentRole.Function, content: message.Content)); // Send to Next LLM @@ -94,8 +95,8 @@ public partial class RoutingService } else { - dialogs.Add(RoleDialogModel.From(message, - role: AgentRole.Assistant, + dialogs.Add(RoleDialogModel.From(message, + role: AgentRole.Assistant, content: message.Content)); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index d9cd266e..a241fbe2 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -172,13 +172,13 @@ public partial class RoutingService : IRoutingService 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 partial class RoutingService : IRoutingService 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 eca5ac78..9147979f 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 158bf965..20874a60 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 2f74b54b..449bd4d7 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 33f8db9f..a1025e50 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 b5052965..856203ed 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Functions/InputUserTextFn.cs @@ -27,6 +27,7 @@ public class InputUserTextFn : IFunctionCallback await _driver.InputUserText(agent, args, message.MessageId); message.Content = $"Input text \"{args.InputText}\" successfully."; + return true; } }