diff --git a/docs/agent/assets/routing-calling-stack.png b/docs/agent/assets/routing-calling-stack.png new file mode 100644 index 00000000..7d0dfc21 Binary files /dev/null and b/docs/agent/assets/routing-calling-stack.png differ diff --git a/docs/agent/router.md b/docs/agent/router.md index 3c871664..0acbdc08 100644 --- a/docs/agent/router.md +++ b/docs/agent/router.md @@ -48,4 +48,24 @@ public class TransferToCsrRoutingHandler : IRoutingHandler } ``` +## Routing - Redirection + +Router will maintain an Agent call stack and automatically set the currently active Agent based on the conversation status and redirection configuration. + +![calling stack](./assets/routing-calling-stack.png) + +You can configure redirection rules based on your business needs. + +```json +{ + "routingRules": [ + { + "field": "order_number", + "required": true, + "description": "pizza order number", + "redirectTo": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd" + } + ] +} +``` diff --git a/docs/architecture/assets/.$routing-redirection.drawio.bkp b/docs/architecture/assets/.$routing-redirection.drawio.bkp new file mode 100644 index 00000000..cc9b5643 --- /dev/null +++ b/docs/architecture/assets/.$routing-redirection.drawio.bkp @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/assets/.$routing-redirection.drawio.dtmp b/docs/architecture/assets/.$routing-redirection.drawio.dtmp new file mode 100644 index 00000000..903528a9 --- /dev/null +++ b/docs/architecture/assets/.$routing-redirection.drawio.dtmp @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/assets/routing-reasoner.png b/docs/architecture/assets/routing-reasoner.png new file mode 100644 index 00000000..abee1360 Binary files /dev/null and b/docs/architecture/assets/routing-reasoner.png differ diff --git a/docs/architecture/assets/routing-redirection.drawio b/docs/architecture/assets/routing-redirection.drawio new file mode 100644 index 00000000..e0b1e1ca --- /dev/null +++ b/docs/architecture/assets/routing-redirection.drawio @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture/assets/routing-redirection.png b/docs/architecture/assets/routing-redirection.png new file mode 100644 index 00000000..20e6e9f9 Binary files /dev/null and b/docs/architecture/assets/routing-redirection.png differ diff --git a/docs/architecture/routing.md b/docs/architecture/routing.md index 4ddead4d..a4bbb6ea 100644 --- a/docs/architecture/routing.md +++ b/docs/architecture/routing.md @@ -6,9 +6,12 @@ Routing is an important function that allows multiple Agents to work together to The Routing feature is the core technology used by BotSharp to manage multiple Agents. BotSharp has a built-in intelligent Agent called `Router`. When you enable this function, all user requests will be pre-processed by the Router to determine which Agent to distribute the request to for processing. The advantage of Routing technology is that it can isolate different Agents and allow them to work together to achieve the user's goals. The adoption of `Routing` ensures that Agent can be scalable, flexible and robust enough in enterprise applications. +![routing with reasoning](./assets/routing-redirection.png) + ## Reasoner For simple questions raised by users, the ordinary routing function can already handle it. However, for the scenario where the user has a long description and needs to disassemble the task, ordinary routing cannot handle it. At this time, the `Reasoning` feature needs to be turned on, and LLM will respond according to the problem. The complexity is broken down into different small tasks. These small tasks can be processed by the corresponding Agent. During the processing process, the Router will constantly adjust the next step plan to deal with the different results returned by the Agent. +![routing with reasoning](./assets/routing-reasoner.png) For more **Routing** related information, please go to [Agent Routing](../agent/router.md). \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs index 8daa6109..755e8213 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.GetAgents.cs @@ -7,6 +7,9 @@ namespace BotSharp.Core.Agents.Services; public partial class AgentService { +#if !DEBUG + [MemoryCache(10 * 60)] +#endif public async Task> GetAgents() { var query = from a in _db.Agents diff --git a/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs index df8209e5..bdba221a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs @@ -34,20 +34,26 @@ public class RouteToAgentFn : IFunctionCallback } else { + var db = _services.GetRequiredService(); + var targetAgent = db.Agents.FirstOrDefault(x => x.Name.ToLower() == args.AgentName.ToLower()); + if (targetAgent == null) + { + message.ExecutionData = JsonSerializer.Deserialize(message.FunctionArgs); + return false; + } + var missingfield = HasMissingRequiredField(message, out var agentId); if (missingfield && message.CurrentAgentId != agentId) { + // Stack original Agent + _context.Push(targetAgent.Id); + message.CurrentAgentId = agentId; } else { - var db = _services.GetRequiredService(); - var record = db.Agents.FirstOrDefault(x => x.Name.ToLower() == args.AgentName.ToLower()); - if (record != null) - { - message.CurrentAgentId = record.Id; - message.ExecutionResult = $"Routing to {args.AgentName}"; - } + message.CurrentAgentId = targetAgent.Id; + message.ExecutionResult = $"Routing to {args.AgentName}"; } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetNextInstruction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetNextInstruction.cs index 636d8cac..28d42523 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetNextInstruction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetNextInstruction.cs @@ -18,9 +18,10 @@ public partial class RoutingService var content = $"{prompt} Response must be in JSON format {responseFormat}"; var state = _services.GetRequiredService(); - RoleDialogModel response = default; + var args = new FunctionCallFromLlm(); + if (_settings.UseTextCompletion) { var completion = CompletionProvider.GetTextCompletion(_services, @@ -37,47 +38,58 @@ public partial class RoutingService provider: _settings.Provider, model: _settings.Model); - response = completion.GetChatCompletions(_routerInstance.Router, new List + int retryCount = 0; + + while (retryCount < 3) { - new RoleDialogModel(AgentRole.User, content) - }); + try + { + response = completion.GetChatCompletions(_routerInstance.Router, new List + { + new RoleDialogModel(AgentRole.User, content) + }); + + var pattern = @"\{(?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!))\}"; + response.Content = Regex.Match(response.Content, pattern).Value; + args = JsonSerializer.Deserialize(response.Content); + break; + } + catch (Exception ex) + { + _logger.LogError($"{ex.Message}: {response.Content}"); + args.Function = "response_to_user"; + args.Answer = ex.Message; + args.AgentName = _settings.RouterName; + content += "\r\nPlease response in JSON format."; + } + finally + { + retryCount++; + } + } } - var args = new FunctionCallFromLlm(); - try - { #if DEBUG - Console.WriteLine(response.Content, Color.Gray); + Console.WriteLine(response.Content, Color.Gray); #else - _logger.LogInformation(response.Content); + _logger.LogInformation(response.Content); #endif - var pattern = @"\{(?:[^{}]|(?\{)|(?<-open>\}))+(?(open)(?!))\}"; - response.Content = Regex.Match(response.Content, pattern).Value; - args = JsonSerializer.Deserialize(response.Content); - // Sometimes it populate malformed Function in Agent name - if (!string.IsNullOrEmpty(args.Function) && args.Function == args.AgentName) - { - args.Function = "route_to_agent"; - _logger.LogWarning($"Captured LLM malformed response"); - } - - // Another case of malformed response - var agentService = _services.GetRequiredService(); - var agents = await agentService.GetAgents(); - if (string.IsNullOrEmpty(args.AgentName) && agents.Select(x => x.Name).Contains(args.Function)) - { - args.AgentName = args.Function; - args.Function = "route_to_agent"; - _logger.LogWarning($"Captured LLM malformed response"); - } - } - catch (Exception ex) + // Sometimes it populate malformed Function in Agent name + if (!string.IsNullOrEmpty(args.Function) && args.Function == args.AgentName) { - _logger.LogError($"{ex.Message}: {response.Content}"); - args.Function = "response_to_user"; - args.Answer = ex.Message; - args.AgentName = _settings.RouterName; + args.Function = "route_to_agent"; + _logger.LogWarning($"Captured LLM malformed response"); + } + + // Another case of malformed response + var agentService = _services.GetRequiredService(); + var agents = await agentService.GetAgents(); + if (string.IsNullOrEmpty(args.AgentName) && agents.Select(x => x.Name).Contains(args.Function)) + { + args.AgentName = args.Function; + args.Function = "route_to_agent"; + _logger.LogWarning($"Captured LLM malformed response"); } if (args.Arguments != null) diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 87ef9645..aa1a2523 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -62,7 +62,7 @@ public partial class RoutingService response.Role = recursiveResponse.Role; response.Content = recursiveResponse.Content; response.ExecutionResult = recursiveResponse.ExecutionResult; - response.ExecutionData = recursiveResponse.ExecutionData; + response.ExecutionData = recursiveResponse.ExecutionData ?? response.ExecutionData; response.StopCompletion = recursiveResponse.StopCompletion; } }