Allow task agent to fallback to predefined router.

This commit is contained in:
Haiping Chen 2024-01-26 16:23:10 -06:00
parent 29a2ffe706
commit e1a948c9c7
15 changed files with 143 additions and 21 deletions

View file

@ -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";
/// <summary>
/// ParameterPropertyDef

View file

@ -0,0 +1,14 @@
namespace BotSharp.Abstraction.Routing.Enums;
public class RuleType
{
/// <summary>
/// Fallback to redirect agent
/// </summary>
public const string Fallback = "fallback";
/// <summary>
/// Redirect to other agent if data validation failed
/// </summary>
public const string DataValidation = "data-validation";
}

View file

@ -13,8 +13,20 @@ public interface IRoutingService
/// <returns></returns>
RoutableAgent[] GetRoutableAgents(List<string> profiles);
RoutingRule[] GetRulesByName(string name);
/// <summary>
/// Get rules by agent name
/// </summary>
/// <param name="name">agent name</param>
/// <returns></returns>
RoutingRule[] GetRulesByAgentName(string name);
/// <summary>
/// Get rules by agent id
/// </summary>
/// <param name="id">agent id </param>
/// <returns></returns>
RoutingRule[] GetRulesByAgentId(string id);
List<RoutingHandlerDef> GetHandlers();
void ResetRecursiveCounter();
Task<bool> InvokeAgent(string agentId, List<RoleDialogModel> dialogs);

View file

@ -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();

View file

@ -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; }
/// <summary>
/// Field type: string, number, object
/// </summary>
public string Type { get; set; } = "string";
public string FieldType { get; set; } = "string";
public bool Required { get; set; }

View file

@ -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<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
var agentService = _services.GetRequiredService<IAgentService>();
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<RoutingContext>();
routing.Replace(targetAgent.Id);
var router = _services.GetRequiredService<IRoutingService>();
message.CurrentAgentId = targetAgent.Id;
var response = await router.InstructLoop(message);
message.Content = response.Content;
message.StopCompletion = true;
return true;
}
}

View file

@ -91,7 +91,7 @@ public class RouteToAgentFn : IFunctionCallback
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
var routing = _services.GetRequiredService<IRoutingService>();
var routingRules = routing.GetRulesByName(args.AgentName);
var routingRules = routing.GetRulesByAgentName(args.AgentName);
if (routingRules == null || !routingRules.Any())
{

View file

@ -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<FunctionDef> 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<IRoutingService>();
var rule = routing.GetRulesByAgentId(_agent.Id)
.FirstOrDefault(x => x.Type == RuleType.Fallback);
if (rule != null)
{
var agentService = _services.GetRequiredService<IAgentService>();
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<JsonDocument>(json)
}
});
}
}
return base.OnFunctionsLoaded(functions);
}
}

View file

@ -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<RoutingContext>();
// Find response template
var templateService = _services.GetRequiredService<IResponseTemplateService>();
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));
}

View file

@ -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())

View file

@ -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 }}
{{ conversation }}

View file

@ -1 +1 @@
Break down the users most recent needs and figure out the next steps. Response must be in appropriate JSON format.
Break down the users most recent needs and figure out the next steps.

View file

@ -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.

View file

@ -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.
What is the next step based on the CONVERSATION?

View file

@ -27,6 +27,7 @@ public class InputUserTextFn : IFunctionCallback
await _driver.InputUserText(agent, args, message.MessageId);
message.Content = $"Input text \"{args.InputText}\" successfully.";
return true;
}
}