lazy routing (WIP)
This commit is contained in:
parent
8e668b8627
commit
51d933e3db
|
|
@ -8,6 +8,9 @@ public class StateConst
|
|||
public const string NEXT_ACTION_REASON = "next_action_reason";
|
||||
public const string USER_GOAL_AGENT = "user_goal_agent";
|
||||
public const string AGENT_REDIRECTION_REASON = "agent_redirection_reason";
|
||||
// lazy or eager
|
||||
public const string ROUTING_MODE = "routing_mode";
|
||||
public const string LAZY_ROUTING_AGENT_ID = "lazy_routing_agent_id";
|
||||
|
||||
public const string LANGUAGE = "language";
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ public interface IRoutingContext
|
|||
bool IsEmpty { get; }
|
||||
string IntentName { get; set; }
|
||||
int AgentCount { get; }
|
||||
void Push(string agentId, string? reason = null);
|
||||
void Pop(string? reason = null);
|
||||
void PopTo(string agentId, string reason);
|
||||
void Replace(string agentId, string? reason = null);
|
||||
void Push(string agentId, string? reason = null, bool updateLazyRouting = true);
|
||||
void Pop(string? reason = null, bool updateLazyRouting = true);
|
||||
void PopTo(string agentId, string reason, bool updateLazyRouting = true);
|
||||
void Replace(string agentId, string? reason = null, bool updateLazyRouting = true);
|
||||
void Empty(string? reason = null);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace BotSharp.Abstraction.Routing.Models;
|
||||
|
||||
public class FallbackArgs
|
||||
{
|
||||
[JsonPropertyName("fallback_reason")]
|
||||
public string Reason { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("user_question")]
|
||||
public string Question { get; set; } = null;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(TargetFramework)</TargetFramework>
|
||||
|
|
@ -66,6 +66,8 @@
|
|||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.get_remaining_task.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\reasoner.sequential.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\agent.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-fallback_to_router.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-redirect_to_agent.json" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\agent.json" />
|
||||
<None Remove="data\agents\01dcc3e5-0af7-49e6-ad7a-a760bd12dc4b\functions.json" />
|
||||
|
|
@ -82,6 +84,7 @@
|
|||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\response_with_function.liquid" />
|
||||
<None Remove="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\select_file_prompt.liquid" />
|
||||
<None Remove="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-routing-fallback_to_router.fn.liquid" />
|
||||
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json" />
|
||||
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\instructions\instruction.liquid" />
|
||||
<None Remove="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\templates\instruction.executor.liquid" />
|
||||
|
|
@ -146,6 +149,15 @@
|
|||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\templates\translation_prompt.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-routing-fallback_to_router.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a\functions\route_to_agent.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\templates\util-routing-fallback_to_router.fn.liquid">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="data\agents\dfd9b46d-d00c-40af-8a75-3fbdc2b89869\agent.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Messaging;
|
||||
using BotSharp.Abstraction.Messaging.Models.RichContent;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
|
|
@ -36,7 +37,17 @@ public partial class ConversationService
|
|||
// Enqueue receiving agent first in case it stop completion by OnMessageReceived
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.SetMessageId(_conversationId, message.MessageId);
|
||||
routing.Context.Push(agent.Id, reason: "request started");
|
||||
|
||||
// Check the routing mode
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
|
||||
routing.Context.Push(agent.Id, reason: "request started", updateLazyRouting: false);
|
||||
|
||||
if (routingMode == "lazy")
|
||||
{
|
||||
message.CurrentAgentId = states.GetState(StateConst.LAZY_ROUTING_AGENT_ID, message.CurrentAgentId);
|
||||
routing.Context.Push(message.CurrentAgentId, reason: "lazy routing", updateLazyRouting: false);
|
||||
}
|
||||
|
||||
// Save payload in order to assign the payload before hook is invoked
|
||||
if (replyMessage != null && !string.IsNullOrEmpty(replyMessage.Payload))
|
||||
|
|
@ -77,7 +88,7 @@ public partial class ConversationService
|
|||
{
|
||||
agent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||
}
|
||||
|
||||
|
||||
if (agent.Type == AgentType.Routing)
|
||||
{
|
||||
response = await routing.InstructLoop(message, dialogs);
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ namespace BotSharp.Core.Routing.Functions;
|
|||
|
||||
public class FallbackToRouterFn : IFunctionCallback
|
||||
{
|
||||
public string Name => "fallback_to_router";
|
||||
public string Name => "util-routing-fallback_to_router";
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public FallbackToRouterFn(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
|
|
@ -14,30 +15,10 @@ public class FallbackToRouterFn : IFunctionCallback
|
|||
|
||||
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
|
||||
{
|
||||
AgentNames = [args.AgentName]
|
||||
});
|
||||
var targetAgent = agents.Items.FirstOrDefault();
|
||||
if (targetAgent == null)
|
||||
{
|
||||
message.Content = $"Can't find routing agent {args.AgentName}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var conv = _services.GetRequiredService<IConversationService>();
|
||||
var dialogs = conv.GetDialogHistory();
|
||||
|
||||
var args = JsonSerializer.Deserialize<FallbackArgs>(message.FunctionArgs);
|
||||
var routing = _services.GetRequiredService<IRoutingService>();
|
||||
routing.Context.Replace(targetAgent.Id);
|
||||
message.CurrentAgentId = targetAgent.Id;
|
||||
|
||||
var response = await routing.InstructLoop(message, dialogs);
|
||||
|
||||
message.Content = response.Content;
|
||||
message.StopCompletion = true;
|
||||
routing.Context.PopTo(routing.Context.EntryAgentId, "pop to entry agent");
|
||||
message.Content = args.Question;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
namespace BotSharp.Core.Routing.Hooks;
|
||||
|
||||
public class RoutingUtilityHook : IAgentUtilityHook
|
||||
{
|
||||
private static string PREFIX = "util-routing-";
|
||||
private static string REDIRECT_TO_AGENT = $"{PREFIX}redirect_to_agent";
|
||||
private static string FALLBACK_TO_ROUTER = $"{PREFIX}fallback_to_router";
|
||||
|
||||
public void AddUtilities(List<AgentUtility> utilities)
|
||||
{
|
||||
var utility = new AgentUtility
|
||||
{
|
||||
Name = "routing.tools",
|
||||
Functions = [new($"{REDIRECT_TO_AGENT}"), new($"{FALLBACK_TO_ROUTER}")],
|
||||
Templates = [new($"{REDIRECT_TO_AGENT}.fn"), new($"{FALLBACK_TO_ROUTER}.fn")]
|
||||
};
|
||||
|
||||
utilities.Add(utility);
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ public class NaiveReasoner : IRoutingReasoner
|
|||
};
|
||||
var response = await completion.GetChatCompletions(router, dialogs);
|
||||
|
||||
inst = response.Content.JsonContent<FunctionCallFromLlm>();
|
||||
inst = (response.FunctionArgs ?? response.Content).JsonContent<FunctionCallFromLlm>();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using BotSharp.Abstraction.Infrastructures.Enums;
|
||||
using BotSharp.Abstraction.Routing.Settings;
|
||||
|
||||
namespace BotSharp.Core.Routing;
|
||||
|
|
@ -79,7 +80,7 @@ public class RoutingContext : IRoutingContext
|
|||
/// </summary>
|
||||
/// <param name="agentId">Id or Name</param>
|
||||
/// <param name="reason"></param>
|
||||
public void Push(string agentId, string? reason = null)
|
||||
public void Push(string agentId, string? reason = null, bool updateLazyRouting = true)
|
||||
{
|
||||
// Convert id to name
|
||||
if (!Guid.TryParse(agentId, out _))
|
||||
|
|
@ -99,13 +100,15 @@ public class RoutingContext : IRoutingContext
|
|||
HookEmitter.Emit<IRoutingHook>(_services, async hook =>
|
||||
await hook.OnAgentEnqueued(agentId, preAgentId, reason: reason)
|
||||
).Wait();
|
||||
|
||||
UpdateLazyRoutingAgent(updateLazyRouting);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pop current agent
|
||||
/// </summary>
|
||||
public void Pop(string? reason = null)
|
||||
public void Pop(string? reason = null, bool updateLazyRouting = true)
|
||||
{
|
||||
if (_stack.Count == 0)
|
||||
{
|
||||
|
|
@ -149,15 +152,17 @@ public class RoutingContext : IRoutingContext
|
|||
_stack.Push(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateLazyRoutingAgent(updateLazyRouting);
|
||||
}
|
||||
|
||||
public void PopTo(string agentId, string reason)
|
||||
public void PopTo(string agentId, string reason, bool updateLazyRouting = true)
|
||||
{
|
||||
var currentAgentId = GetCurrentAgentId();
|
||||
while (!string.IsNullOrEmpty(currentAgentId) &&
|
||||
currentAgentId != agentId)
|
||||
{
|
||||
Pop(reason);
|
||||
Pop(reason, updateLazyRouting: updateLazyRouting);
|
||||
currentAgentId = GetCurrentAgentId();
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +186,7 @@ public class RoutingContext : IRoutingContext
|
|||
return _stack.ToArray().Contains(agentId);
|
||||
}
|
||||
|
||||
public void Replace(string agentId, string? reason = null)
|
||||
public void Replace(string agentId, string? reason = null, bool updateLazyRouting = true)
|
||||
{
|
||||
var fromAgent = agentId;
|
||||
var toAgent = agentId;
|
||||
|
|
@ -200,6 +205,8 @@ public class RoutingContext : IRoutingContext
|
|||
await hook.OnAgentReplaced(fromAgent, toAgent, reason: reason)
|
||||
).Wait();
|
||||
}
|
||||
|
||||
UpdateLazyRoutingAgent(updateLazyRouting);
|
||||
}
|
||||
|
||||
public void Empty(string? reason = null)
|
||||
|
|
@ -275,4 +282,24 @@ public class RoutingContext : IRoutingContext
|
|||
{
|
||||
_dialogs = [];
|
||||
}
|
||||
|
||||
private void UpdateLazyRoutingAgent(bool updateLazyRouting)
|
||||
{
|
||||
if (!updateLazyRouting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set next handling agent for lazy routing mode
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard");
|
||||
if (routingMode == "lazy")
|
||||
{
|
||||
var agentId = GetCurrentAgentId();
|
||||
if (agentId != BuiltInAgentId.Fallback)
|
||||
{
|
||||
states.SetState(StateConst.LAZY_ROUTING_AGENT_ID, agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,7 @@ public class RoutingPlugin : IBotSharpPlugin
|
|||
services.AddScoped<IRoutingReasoner, HFReasoner>();
|
||||
|
||||
services.AddScoped<IRoutingReasoner, OneStepForwardReasoner>();
|
||||
|
||||
services.AddScoped<IAgentUtilityHook, RoutingUtilityHook>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ public partial class RoutingService
|
|||
// Handle output routing exception.
|
||||
if (agent.Type == AgentType.Routing)
|
||||
{
|
||||
response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?";
|
||||
// Forgot about what situation needs to handle in this way
|
||||
// response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?";
|
||||
}
|
||||
|
||||
message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "route_to_agent",
|
||||
"description": "Route request to appropriate AI agent.",
|
||||
"visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"next_action_agent": {
|
||||
"type": "string",
|
||||
"description": "Agent for next action based on user latest response"
|
||||
},
|
||||
"next_action_reason": {
|
||||
"type": "string",
|
||||
"description": "The reason why route to this agent."
|
||||
},
|
||||
"user_goal_agent": {
|
||||
"type": "string",
|
||||
"description": "Agent who can acheive user initial task."
|
||||
},
|
||||
"conversation_end": {
|
||||
"type": "boolean",
|
||||
"description": "User is ending the conversation."
|
||||
},
|
||||
"args": {
|
||||
"type": "object",
|
||||
"description": "Required parameters of next action agent"
|
||||
}
|
||||
},
|
||||
"required": [ "next_action_agent", "user_goal_agent", "args" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ Follow these steps to handle user request:
|
|||
{%- endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if routing_mode != 'lazy' %}
|
||||
[FUNCTIONS]
|
||||
{% for handler in routing_handlers -%}
|
||||
# {{ handler.description}}
|
||||
|
|
@ -26,6 +27,7 @@ Parameters:
|
|||
{%- endif %}
|
||||
{{ "\r\n" }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
|
||||
[AGENTS]
|
||||
{% for agent in routing_agents -%}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "util-routing-fallback_to_router",
|
||||
"description": "Get the appropriate agent who can handle the user request.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fallback_reason": {
|
||||
"type": "string",
|
||||
"description": "The reason why you need to reach out to other agent."
|
||||
},
|
||||
"user_question": {
|
||||
"type": "string",
|
||||
"description": "User question or statement."
|
||||
}
|
||||
},
|
||||
"required": [ "user_question" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
"If you're unsure whether you understand the user's request or if the user brings up an unrelated topic, call the function `util-routing-fallback_to_router` to get the appropriate agent from the router."
|
||||
Loading…
Reference in a new issue