Skip to content

Allow task agent to fallback to predefined router. #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using BotSharp.Abstraction.Routing.Enums;

namespace BotSharp.Abstraction.Routing.Models;

public class RoutingRule
Expand All @@ -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; }

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private bool HasMissingRequiredField(RoleDialogModel message, out string agentId
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())
{
Expand Down
41 changes: 37 additions & 4 deletions src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -33,11 +35,42 @@ public override bool OnInstructionLoaded(string template, Dictionary<string, obj

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);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Templating;

Expand Down Expand Up @@ -70,7 +71,7 @@ private async Task<bool> InvokeFunction(RoleDialogModel message, List<RoleDialog
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);
Expand All @@ -83,8 +84,8 @@ private async Task<bool> InvokeFunction(RoleDialogModel message, List<RoleDialog
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
Expand All @@ -94,8 +95,8 @@ private async Task<bool> InvokeFunction(RoleDialogModel message, List<RoleDialog
}
else
{
dialogs.Add(RoleDialogModel.From(message,
role: AgentRole.Assistant,
dialogs.Add(RoleDialogModel.From(message,
role: AgentRole.Assistant,
content: message.Content));
}

Expand Down
6 changes: 3 additions & 3 deletions src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,13 @@ public RoutableAgent[] GetRoutableAgents(List<string> 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()
Expand All @@ -202,7 +202,7 @@ public RoutableAgent[] GetRoutableAgents(List<string> profiles)
return routableAgents;
}

public RoutingRule[] GetRulesByName(string name)
public RoutingRule[] GetRulesByAgentName(string name)
{
return GetRoutingRecords()
.Where(x => x.AgentName.ToLower() == name.ToLower())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down Expand Up @@ -36,4 +37,4 @@ Optional args:
{% endfor %}

[CONVERSATION]
{{ conversation }}
{{ conversation }}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Break down the user’s most recent needs and figure out the next steps. Response must be in appropriate JSON format.
Break down the user’s most recent needs and figure out the next steps.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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?
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public async Task<bool> Execute(RoleDialogModel message)
await _driver.InputUserText(agent, args, message.MessageId);

message.Content = $"Input text \"{args.InputText}\" successfully.";

return true;
}
}