From 6fa6aa180dbd3fb8b596483df51585a900c1bf1f Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Sat, 26 Apr 2025 13:05:15 -0500 Subject: [PATCH 1/4] Deprecate routing handler. --- .../Conversations/IConversationHook.cs | 15 --- .../Functions/Models/FunctionCallFromLlm.cs | 9 +- .../Routing/IRoutingHandler.cs | 20 ---- .../Routing/IRoutingService.cs | 2 - .../Routing/Models/ResponseUserArgs.cs | 7 ++ .../Routing/Models/RoutingArgs.cs | 24 +---- .../BotSharp.Core/BotSharpCoreExtensions.cs | 11 --- .../ConversationService.SendMessage.cs | 8 -- .../Routing/Functions/ResponseToUserFn.cs | 4 +- .../ContinueExecuteTaskRoutingHandler.cs | 50 ---------- .../InterruptTaskExecutionRoutingHandler.cs | 37 ------- .../RetrieveDataFromAgentRoutingHandler.cs | 59 ----------- .../Handlers/RouteToAgentRoutingHandler.cs | 99 ------------------- .../Routing/Hooks/RoutingAgentHook.cs | 1 - .../Routing/Reasoning/HFReasoner.cs | 38 ++----- .../Routing/Reasoning/InstructExecutor.cs | 48 +++++++-- .../Routing/Reasoning/NaiveReasoner.cs | 47 ++------- .../Reasoning/OneStepForwardReasoner.cs | 44 +++------ .../Routing/RoutingService.InstructLoop.cs | 9 +- .../BotSharp.Core/Routing/RoutingService.cs | 23 +---- .../functions/response_to_user.json | 7 +- .../functions/route_to_agent.json | 1 - .../instructions/instruction.liquid | 15 --- .../Sequential/SequentialPlanner.cs | 53 ++++------ .../SqlGeneration/SqlGenerationPlanner.cs | 6 +- .../TwoStaging/TwoStageTaskPlanner.cs | 6 +- src/WebStarter/appsettings.json | 4 +- .../BotSharp.Plugin.PizzaBot.csproj | 7 +- .../Functions/PlaceOrderFn.cs | 6 +- .../agent.json | 6 +- .../agent.json | 2 +- .../functions/get_order_status.json | 2 +- .../agent.json | 2 +- .../{place_an_order.json => place_order.json} | 2 +- .../instructions/instruction.liquid | 13 ++- .../responses/func.get_pizza_price.0.liquid | 13 --- 36 files changed, 136 insertions(+), 564 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Routing/Models/ResponseUserArgs.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs rename tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/{place_an_order.json => place_order.json} (94%) delete mode 100644 tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/responses/func.get_pizza_price.0.liquid diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs index c764f391..b722d3fa 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs @@ -69,21 +69,6 @@ public interface IConversationHook Task OnResponseGenerated(RoleDialogModel message); - /// - /// LLM detected user requested a new task different from previous topic. - /// - /// - /// - Task OnNewTaskDetected(RoleDialogModel message, string reason); - - /// - /// LLM detected the current task is completed. - /// It's useful for the situation of multiple tasks in the same conversation. - /// - /// - /// - Task OnTaskCompleted(RoleDialogModel message); - /// /// LLM detected the whole conversation is going to be end. /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs index 661a1437..0bb248f4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -36,13 +36,6 @@ public class FunctionCallFromLlm : RoutingArgs { var route = string.IsNullOrEmpty(AgentName) ? "" : $""; - if (string.IsNullOrEmpty(Response)) - { - return $"[{Function} {route} {JsonSerializer.Serialize(Arguments)}]: {Question}"; - } - else - { - return $"[{Function} {route} {JsonSerializer.Serialize(Arguments)}]: {Question} => {Response}"; - } + return $"[{Function} {route} {JsonSerializer.Serialize(Arguments)}]: {Question}"; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs deleted file mode 100644 index 95cddf2e..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using BotSharp.Abstraction.Functions.Models; - -namespace BotSharp.Abstraction.Routing; - -/// -/// The routing handler will be injected to Router's FUNCTIONS section of the system prompt -/// So the handler will be invoked by LLM autonomously. -/// -public interface IRoutingHandler -{ - string Name { get; } - string Description { get; } - List Planers => null; - bool Enabled => true; - List Parameters => new List(); - - void SetDialogs(List dialogs); - - Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message); -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index c0f7c81c..d98dd495 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -26,8 +26,6 @@ public interface IRoutingService /// RoutingRule[] GetRulesByAgentId(string id); - List GetHandlers(Agent router); - //void ResetRecursiveCounter(); //int GetRecursiveCounter(); //void SetRecursiveCounter(int counter); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/ResponseUserArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/ResponseUserArgs.cs new file mode 100644 index 00000000..4a3a027b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/ResponseUserArgs.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Routing.Models; + +public class ResponseUserArgs +{ + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs index bc9a6fa8..44d41f31 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs @@ -15,19 +15,6 @@ public class RoutingArgs [JsonPropertyName("conversation_end")] public bool ConversationEnd { get; set; } - [JsonPropertyName("task_completed")] - public bool TaskCompleted { get; set; } - - [JsonPropertyName("is_new_task")] - public bool IsNewTask { get; set; } - - /// - /// The content of replying to user - /// - [JsonPropertyName("response")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Response { get; set; } = string.Empty; - /// /// Agent for next action based on user latest response /// @@ -38,10 +25,12 @@ public class RoutingArgs /// /// Agent who can achieve user original goal /// + [Obsolete("Will be replaced by dedicate Reasoner")] [JsonPropertyName("user_goal_agent")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string OriginalAgent { get; set; } = string.Empty; + [Obsolete("Will be replaced by dedicate Reasoner")] [JsonPropertyName("user_goal_description")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string UserGoal { get; set; } = string.Empty; @@ -50,13 +39,6 @@ public class RoutingArgs { var route = string.IsNullOrEmpty(AgentName) ? "" : $""; - if (string.IsNullOrEmpty(Response)) - { - return $"[{Function} {route}]"; - } - else - { - return $"[{Function} {route}] => {Response}"; - } + return $"[{Function} {route}]"; } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index cd85e358..19b6df53 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -150,17 +150,6 @@ public static class BotSharpCoreExtensions var loader = new PluginLoader(services, config, pluginSettings); loader.Load(assembly => { - // Register routing handlers - var handlers = assembly.GetTypes() - .Where(x => x.IsClass) - .Where(x => x.GetInterface(nameof(IRoutingHandler)) != null) - .ToArray(); - - foreach (var handler in handlers) - { - services.AddScoped(typeof(IRoutingHandler), handler); - } - // Register function callback var functions = assembly.GetTypes() .Where(x => x.IsClass diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 3967aaaa..6a844c41 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -155,14 +155,6 @@ public partial class ConversationService var conversation = _services.GetRequiredService(); var updatedConversation = await conversation.UpdateConversationTitle(_conversationId, response.Instruction.NextActionReason); - // Emit conversation task completed hook - if (response.Instruction.TaskCompleted) - { - await HookEmitter.Emit(_services, async hook => - await hook.OnTaskCompleted(response) - ); - } - // Emit conversation ending hook if (response.Instruction.ConversationEnd) { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/ResponseToUserFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/ResponseToUserFn.cs index 0ee3e585..fe322f31 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/ResponseToUserFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/ResponseToUserFn.cs @@ -20,8 +20,8 @@ public class ResponseToUserFn : IFunctionCallback public Task Execute(RoleDialogModel message) { - var args = JsonSerializer.Deserialize(message.FunctionArgs); - message.Content = args.Response; + var args = JsonSerializer.Deserialize(message.FunctionArgs); + message.Content = args.Content; message.Handled = true; message.StopCompletion = true; return Task.FromResult(true); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs deleted file mode 100644 index ea50ef72..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Models; -using BotSharp.Abstraction.Repositories; -using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Reasoning; - -namespace BotSharp.Core.Routing.Handlers; - -public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase//, IRoutingHandler -{ - public string Name => "continue_execute_task"; - - public string Description => "Continue to execute user's request without further information retrival."; - - public List Parameters => new List - { - new ParameterPropertyDef("next_action_agent", "agent for next action based on user latest response"), - new ParameterPropertyDef("user_goal_agent", "agent who can achieve user original goal"), - new ParameterPropertyDef("reason", "why continue to execute current task"), - new ParameterPropertyDef("args", "required parameters extracted from question") - { - Type = "object" - } - }; - - public List Planers => new List - { - nameof(HFReasoner) - }; - - public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var db = _services.GetRequiredService(); - var filter = new AgentFilter { AgentNames = [inst.AgentName] }; - var record = db.GetAgents(filter).FirstOrDefault(); - - message.FunctionName = inst.Function; - message.CurrentAgentId = record.Id; - message.FunctionArgs = JsonSerializer.Serialize(inst.Arguments); - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs deleted file mode 100644 index 0d11f296..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs +++ /dev/null @@ -1,37 +0,0 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Reasoning; - -namespace BotSharp.Core.Routing.Handlers; - -public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase//, IRoutingHandler -{ - public string Name => "interrupt_task_execution"; - - public string Description => "Can't continue user's request becauase the requirements are not met."; - - public List Parameters => new List - { - new ParameterPropertyDef("reason", "the reason why the request is interrupted"), - new ParameterPropertyDef("answer", "the content response to user") - }; - - public List Planers => new List - { - nameof(HFReasoner) - }; - - public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - message.FunctionName = inst.Function; - message.StopCompletion = true; - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs deleted file mode 100644 index 038488eb..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs +++ /dev/null @@ -1,59 +0,0 @@ -using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Reasoning; - -namespace BotSharp.Core.Routing.Handlers; - -/// -/// Retrieve information from specific agent -/// -public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase//, IRoutingHandler -{ - public string Name => "retrieve_data_from_agent"; - - public string Description => "Retrieve data from appropriate agent."; - - public List Parameters => new List - { - new ParameterPropertyDef("reason", "why choose this function"), - new ParameterPropertyDef("question", "the question you will ask the next action agent to get the necessary information"), - new ParameterPropertyDef("next_action_agent", "agent that can handle the question"), - new ParameterPropertyDef("user_goal_agent", "agent that can achieve user original goal"), - new ParameterPropertyDef("args", "required parameters extracted from question and hand over to the next agent") - { - Type = "object" - } - }; - - public List Planers => new List - { - nameof(HFReasoner) - }; - - public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var context = _services.GetRequiredService(); - var agentId = context.GetCurrentAgentId(); - var dialogs = new List - { - new RoleDialogModel(AgentRole.User, inst.Question) - { - CurrentAgentId = agentId, - MessageId = message.MessageId - } - }; - - var ret = await routing.InvokeAgent(agentId, dialogs); - var response = dialogs.Last(); - inst.Response = response.Content; - - // Add final response to parent dialog - _dialogs.Add(response); - - return ret; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs deleted file mode 100644 index 1dc808a0..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using BotSharp.Abstraction.Infrastructures.Enums; -using BotSharp.Abstraction.Routing.Settings; - -namespace BotSharp.Core.Routing.Handlers; - -public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler -{ - public string Name => "route_to_agent"; - - public string Description => "Route request to appropriate virtual agent."; - - public List Parameters => new List - { - new ParameterPropertyDef("next_action_reason", - "the reason why route to this virtual agent.", - required: true), - new ParameterPropertyDef("next_action_agent", - "agent for next action based on user latest response, if user is replying last agent's question, you must route to this agent.", - required: true), - new ParameterPropertyDef("args", - "useful parameters of next action agent, format: { }", - type: "object"), - new ParameterPropertyDef("user_goal_description", - "user goal based on user initial task.", - required: true), - new ParameterPropertyDef("user_goal_agent", - "agent who can acheive user initial task.", - required: true), - new ParameterPropertyDef("conversation_end", - "user is ending the conversation.", - type: "boolean", - required: true), - new ParameterPropertyDef("is_new_task", - "whether the user is requesting a new task that is different from the previous topic. Set the first round of conversation to false.", - type: "boolean") - }; - - public RouteToAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) - : base(services, logger, settings) - { - } - - public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) - { - var states = _services.GetRequiredService(); - var goalAgent = states.GetState(StateConst.EXPECTED_GOAL_AGENT); - if (!string.IsNullOrEmpty(goalAgent) && inst.OriginalAgent != goalAgent) - { - inst.OriginalAgent = goalAgent; - // Emit hook - await HookEmitter.Emit(_services, async hook => - await hook.OnRoutingInstructionRevised(inst, message) - ); - } - - if (inst.IsNewTask) - { - await HookEmitter.Emit(_services, async hook => - await hook.OnNewTaskDetected(message, inst.NextActionReason) - ); - } - - message.FunctionArgs = JsonSerializer.Serialize(inst); - if (message.FunctionName != null) - { - var msg = RoleDialogModel.From(message, role: AgentRole.Function); - await routing.InvokeFunction(message.FunctionName, msg); - } - - var agentId = routing.Context.GetCurrentAgentId(); - - // Update next action agent's name - var agentService = _services.GetRequiredService(); - var agent = await agentService.GetAgent(agentId); - inst.AgentName = agent.Name; - - if (inst.ExecutingDirectly) - { - message.Content = inst.Question; - } - - if (agent.Disabled) - { - var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent."; - - message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: content); - _dialogs.Add(message); - } - else - { - var ret = await routing.InvokeAgent(agentId, _dialogs); - } - - var response = _dialogs.Last(); - inst.Response = response.Content; - - return true; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index f83afedf..53a54669 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -59,7 +59,6 @@ public class RoutingAgentHook : AgentHookBase } dict["routing_agents"] = agents; - dict["routing_handlers"] = routing.GetHandlers(_agent); return base.OnInstructionLoaded(template, dict); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs index fc7c1009..55867e00 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs @@ -38,43 +38,21 @@ public class HFReasoner : IRoutingReasoner { var next = GetNextStepPrompt(router); - RoleDialogModel response = default; - var inst = new FunctionCallFromLlm(); - var completion = CompletionProvider.GetChatCompletion(_services, provider: router?.LlmConfig?.Provider, model: router?.LlmConfig?.Model); - int retryCount = 0; - while (retryCount < 3) + dialogs = new List { - try + new RoleDialogModel(AgentRole.User, next) { - dialogs = new List - { - new RoleDialogModel(AgentRole.User, next) - { - FunctionName = nameof(HFReasoner), - MessageId = messageId - } - }; - response = await completion.GetChatCompletions(router, dialogs); + FunctionName = nameof(HFReasoner), + MessageId = messageId + } + }; + var response = await completion.GetChatCompletions(router, dialogs); - inst = response.Content.JsonContent(); - break; - } - catch (Exception ex) - { - _logger.LogError($"{ex.Message}: {response.Content}"); - inst.Function = "response_to_user"; - inst.Response = ex.Message; - inst.AgentName = "Router"; - } - finally - { - retryCount++; - } - } + var inst = response.Content.JsonContent(); // Fix LLM malformed response ReasonerHelper.FixMalformedResponse(_services, inst); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs index 6bb23e97..e911437d 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Planning; namespace BotSharp.Core.Routing.Reasoning; @@ -18,17 +19,50 @@ public class InstructExecutor : IExecutor RoleDialogModel message, List dialogs) { - message.Instruction = inst; + var states = _services.GetRequiredService(); + var goalAgent = states.GetState(StateConst.EXPECTED_GOAL_AGENT); + if (!string.IsNullOrEmpty(goalAgent) && inst.OriginalAgent != goalAgent) + { + inst.OriginalAgent = goalAgent; + // Emit hook + await HookEmitter.Emit(_services, async hook => + await hook.OnRoutingInstructionRevised(inst, message) + ); + } - var handlers = _services.GetServices(); - var handler = handlers.FirstOrDefault(x => x.Name == inst.Function); - handler.SetDialogs(dialogs); + message.FunctionArgs = JsonSerializer.Serialize(inst); + if (message.FunctionName != null) + { + var msg = RoleDialogModel.From(message, role: AgentRole.Function); + await routing.InvokeFunction(message.FunctionName, msg); + } - var handled = await handler.Handle(routing, inst, message); + var agentId = routing.Context.GetCurrentAgentId(); + + // Update next action agent's name + var agentService = _services.GetRequiredService(); + var agent = await agentService.GetAgent(agentId); + inst.AgentName = agent.Name; + + if (inst.ExecutingDirectly) + { + message.Content = inst.Question; + } + + if (agent.Disabled) + { + var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent."; + + message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: content); + dialogs.Add(message); + } + else + { + var ret = await routing.InvokeAgent(agentId, dialogs); + } - // For client display purpose var response = dialogs.Last(); - response.MessageId = message.MessageId; + response.Instruction = inst; return response; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs index 8980246e..6adc9bc5 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs @@ -41,53 +41,22 @@ public class NaiveReasoner : IRoutingReasoner { var next = GetNextStepPrompt(router); - var inst = new FunctionCallFromLlm(); - - // text completion - /*var agentService = _services.GetRequiredService(); - var instruction = agentService.RenderedInstruction(router); - var content = $"{instruction}\r\n###\r\n{next}"; - content = content + "\r\nResponse: "; - var completion = CompletionProvider.GetTextCompletion(_services);*/ - // chat completion var completion = CompletionProvider.GetChatCompletion(_services, provider: router?.LlmConfig?.Provider, model: router?.LlmConfig?.Model); - int retryCount = 0; - while (retryCount < 3) + dialogs = new List { - string text = string.Empty; - try + new RoleDialogModel(AgentRole.User, next) { - // text completion - // text = await completion.GetCompletion(content, router.Id, messageId); - dialogs = new List - { - new RoleDialogModel(AgentRole.User, next) - { - FunctionName = nameof(NaiveReasoner), - MessageId = messageId - } - }; - var response = await completion.GetChatCompletions(router, dialogs); + FunctionName = nameof(NaiveReasoner), + MessageId = messageId + } + }; + var response = await completion.GetChatCompletions(router, dialogs); - inst = (response.FunctionArgs ?? response.Content).JsonContent(); - break; - } - catch (Exception ex) - { - _logger.LogError($"{ex.Message}: {text}"); - inst.Function = "response_to_user"; - inst.Response = ex.Message; - inst.AgentName = "Router"; - } - finally - { - retryCount++; - } - } + var inst = (response.FunctionArgs ?? response.Content).JsonContent(); // Fix LLM malformed response ReasonerHelper.FixMalformedResponse(_services, inst); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs index 7e6d2624..800b2457 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs @@ -44,46 +44,26 @@ public class OneStepForwardReasoner : IRoutingReasoner { var next = GetNextStepPrompt(router); - var inst = new FunctionCallFromLlm(); - // chat completion var completion = CompletionProvider.GetChatCompletion(_services, provider: router?.LlmConfig?.Provider, model: router?.LlmConfig?.Model); - int retryCount = 0; - while (retryCount < 3) - { - string text = string.Empty; - try - { - // text completion - // text = await completion.GetCompletion(content, router.Id, messageId); - dialogs = new List - { - new RoleDialogModel(AgentRole.User, next) - { - FunctionName = Name, - MessageId = messageId - } - }; - var response = await completion.GetChatCompletions(router, dialogs); + string text = string.Empty; - inst = response.Content.JsonContent(); - break; - } - catch (Exception ex) + // text completion + // text = await completion.GetCompletion(content, router.Id, messageId); + dialogs = new List + { + new RoleDialogModel(AgentRole.User, next) { - _logger.LogError($"{ex.Message}: {text}"); - inst.Function = "response_to_user"; - inst.Response = ex.Message; - inst.AgentName = "Router"; + FunctionName = Name, + MessageId = messageId } - finally - { - retryCount++; - } - } + }; + var response = await completion.GetChatCompletions(router, dialogs); + + var inst = response.Content.JsonContent(); // Fix LLM malformed response ReasonerHelper.FixMalformedResponse(_services, inst); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs index 07b2f6fa..20aa16b6 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs @@ -58,11 +58,8 @@ public partial class RoutingService // Save states states.SaveStateByArgs(inst.Arguments); -#if DEBUG - Console.WriteLine($"*** Next Instruction *** {inst}"); -#else - _logger.LogInformation($"*** Next Instruction *** {inst}"); -#endif + _logger.LogDebug($"*** Next Instruction *** {inst}"); + await reasoner.AgentExecuting(_router, inst, message, dialogs); // Handover to Task Agent @@ -79,7 +76,7 @@ public partial class RoutingService await reasoner.AgentExecuted(_router, inst, response, dialogs); - if (loopCount >= reasoner.MaxLoopCount || _context.IsEmpty) + if (loopCount >= reasoner.MaxLoopCount || _context.IsEmpty || response.StopCompletion) { break; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 19654deb..253e7b54 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -28,17 +28,12 @@ public partial class RoutingService : IRoutingService public async Task InstructDirect(Agent agent, RoleDialogModel message) { - var handlers = _services.GetServices(); - - var handler = handlers.FirstOrDefault(x => x.Name == "route_to_agent"); - var conv = _services.GetRequiredService(); var storage = _services.GetRequiredService(); storage.Append(conv.ConversationId, message); var dialogs = conv.GetDialogHistory(); Context.SetDialogs(dialogs); - handler.SetDialogs(dialogs); var inst = new FunctionCallFromLlm { @@ -50,7 +45,8 @@ public partial class RoutingService : IRoutingService ExecutingDirectly = true }; - var result = await handler.Handle(this, inst, message); + message.Instruction = inst; + var result = await InvokeFunction("route_to_agent", message); var response = dialogs.Last(); response.MessageId = message.MessageId; @@ -59,21 +55,6 @@ public partial class RoutingService : IRoutingService return response; } - public List GetHandlers(Agent router) - { - var reasoner = GetReasoner(router); - - return _services.GetServices() - .Where(x => x.Planers == null || x.Planers.Contains(reasoner.GetType().Name)) - .Where(x => !string.IsNullOrEmpty(x.Description)) - .Select((x, i) => new RoutingHandlerDef - { - Name = x.Name, - Description = x.Description, - Parameters = x.Parameters - }).ToList(); - } - #if !DEBUG [SharpCache(10)] #endif diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json index a2685810..9e7f8929 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/response_to_user.json @@ -1,15 +1,14 @@ { "name": "response_to_user", - "description": "Response to user without routing to any other agent", - "visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}", + "description": "Response to user without routing to any other agent, user has no specific request.", "parameters": { "type": "object", "properties": { - "response": { + "content": { "type": "string", "description": "Response content" } }, - "required": [ "response" ] + "required": [ "content" ] } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json index 08f938ec..01301bbf 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/functions/route_to_agent.json @@ -1,7 +1,6 @@ { "name": "route_to_agent", "description": "Route request to appropriate AI agent.", - "visibility_expression": "{% if states.routing_mode == 'lazy' %}visible{% endif %}", "parameters": { "type": "object", "properties": { diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid index 99f7a219..d49215d1 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid @@ -18,21 +18,6 @@ Follow these steps to handle user request: {%- endfor %} {% endif %} -{% if routing_mode != 'lazy' %} -[FUNCTIONS] -{% for handler in routing_handlers -%} -# {{ handler.description}} -{% if handler.parameters and handler.parameters != empty -%} -Parameters: - - function: {{ handler.name }} - {% for p in handler.parameters -%} - - {{ p.name }} {% if p.required -%}(required){%- endif %}: {{ p.description }}{{ "\r\n " }} - {%- endfor %} -{%- endif %} -{{ "\r\n" }} -{%- endfor %} -{% endif %} - [AGENTS] {% for agent in routing_agents -%} * Agent: {{ agent.name }} diff --git a/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs index 043c7380..94b4a687 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs @@ -41,7 +41,7 @@ public class SequentialPlanner : ITaskPlanner var decomposation = await GetDecomposedStepAsync(router, messageId, dialogs); if (decomposation.TotalRemainingSteps > 0 && _lastInst != null) { - _lastInst.Response = decomposation.Description; + // _lastInst.Response = decomposation.Description; _lastInst.NextActionReason = $"Having {decomposation.TotalRemainingSteps} steps left."; return _lastInst; } @@ -62,8 +62,6 @@ public class SequentialPlanner : ITaskPlanner var next = GetNextStepPrompt(router); - var inst = new FunctionCallFromLlm(); - // text completion /*var agentService = _services.GetRequiredService(); var instruction = agentService.RenderedInstruction(router); @@ -76,43 +74,26 @@ public class SequentialPlanner : ITaskPlanner provider: router?.LlmConfig?.Provider, model: router?.LlmConfig?.Model); - int retryCount = 0; - while (retryCount < 3) - { - string text = string.Empty; - try - { - // text completion - // text = await completion.GetCompletion(content, router.Id, messageId); - dialogs = new List - { - new RoleDialogModel(AgentRole.User, next) - { - FunctionName = nameof(SequentialPlanner), - MessageId = messageId - } - }; - var response = await completion.GetChatCompletions(router, dialogs); - inst = response.Content.JsonContent(); - break; - } - catch (Exception ex) + string text = string.Empty; + + // text completion + // text = await completion.GetCompletion(content, router.Id, messageId); + dialogs = new List + { + new RoleDialogModel(AgentRole.User, next) { - _logger.LogError($"{ex.Message}: {text}"); - inst.Function = "response_to_user"; - inst.Response = ex.Message; - inst.AgentName = "Router"; + FunctionName = nameof(SequentialPlanner), + MessageId = messageId } - finally - { - retryCount++; - } - } + }; + var response = await completion.GetChatCompletions(router, dialogs); + + var inst = response.Content.JsonContent(); if (decomposation.TotalRemainingSteps > 0) { - inst.Response = decomposation.Description; + // inst.Response = decomposation.Description; inst.NextActionReason = $"{decomposation.TotalRemainingSteps} steps left."; inst.HandleDialogsByPlanner = true; } @@ -125,10 +106,10 @@ public class SequentialPlanner : ITaskPlanner { var taskAgentDialogs = new List { - new RoleDialogModel(AgentRole.User, inst.Response) + /*new RoleDialogModel(AgentRole.User, inst.Response) { MessageId = message.MessageId, - } + }*/ }; return taskAgentDialogs; diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs index 62340e9c..a804b073 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs @@ -42,14 +42,14 @@ public class SqlGenerationPlanner : ITaskPlanner public List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) { - var question = inst.Response; + // var question = inst.Response; var taskAgentDialogs = new List { - new RoleDialogModel(AgentRole.User, question) + /*new RoleDialogModel(AgentRole.User, question) { MessageId = message.MessageId, - } + }*/ }; return taskAgentDialogs; diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs index 6935bfbf..ea89cd71 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -70,14 +70,14 @@ public partial class TwoStageTaskPlanner : ITaskPlanner public List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) { - var question = inst.Response; + // var question = inst.Response; var taskAgentDialogs = new List { - new RoleDialogModel(AgentRole.User, question) + /*new RoleDialogModel(AgentRole.User, question) { MessageId = message.MessageId, - } + }*/ }; return taskAgentDialogs; diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index ce052798..b48d2963 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -254,8 +254,8 @@ "HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c", "EnableTranslator": false, "LlmConfig": { - "Provider": "azure-openai", - "Model": "gpt-4o-mini" + "Provider": "openai", + "Model": "gpt-4.1-nano" } }, diff --git a/tests/BotSharp.Plugin.PizzaBot/BotSharp.Plugin.PizzaBot.csproj b/tests/BotSharp.Plugin.PizzaBot/BotSharp.Plugin.PizzaBot.csproj index d132125f..c056982d 100644 --- a/tests/BotSharp.Plugin.PizzaBot/BotSharp.Plugin.PizzaBot.csproj +++ b/tests/BotSharp.Plugin.PizzaBot/BotSharp.Plugin.PizzaBot.csproj @@ -32,9 +32,7 @@ - - @@ -63,9 +61,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -93,7 +88,7 @@ PreserveNewest - + PreserveNewest diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs index 87466956..e4198f34 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs @@ -1,11 +1,12 @@ using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Infrastructures.Enums; namespace BotSharp.Plugin.PizzaBot.Functions; public class PlaceOrderFn : IFunctionCallback { - public string Name => "place_an_order"; + public string Name => "place_order"; private readonly IServiceProvider _service; public PlaceOrderFn(IServiceProvider service) @@ -19,6 +20,9 @@ public class PlaceOrderFn : IFunctionCallback var state = _service.GetRequiredService(); state.SetState("order_number", "P123-01"); + // Set the next action agent to Payment + state.SetState(StateConst.EXPECTED_ACTION_AGENT, "Payment", activeRounds: 2); + return true; } } diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json index 3a28b3e0..99f1f2fd 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/8970b1e5-d260-4e2c-90b1-f1415a257c18/agent.json @@ -1,7 +1,7 @@ { "id": "8970b1e5-d260-4e2c-90b1-f1415a257c18", "name": "Pizza Bot", - "description": "AI assistant that can help customer place pizza order, make payment or inquiry order status.", + "description": "AI assistant that can help customer place pizza order, make payment or inquiry existing order.", "type": "routing", "inheritAgentId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a", "createdDateTime": "2023-08-18T10:39:32.2349685Z", @@ -11,6 +11,10 @@ "isPublic": true, "profiles": [ "pizza" ], "labels": [ "experiment" ], + "llmConfig": { + "provider": "openai", + "model": "gpt-4.1-nano" + }, "routingRules": [ { "type": "reasoner", diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json index 09f7489b..e9bb78d0 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/agent.json @@ -1,6 +1,6 @@ { "name": "Order Inquiry", - "description": "Check the order status like payment, delivery or baking.", + "description": "Check the existing order status like payment, delivery or baking.", "createdDateTime": "2023-08-18T14:39:32.2349685Z", "updatedDateTime": "2023-08-18T14:39:32.2349686Z", "id": "b284db86-e9c2-4c25-a59e-4649797dd130", diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions/get_order_status.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions/get_order_status.json index 49de63fe..fc7abb8b 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions/get_order_status.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions/get_order_status.json @@ -6,7 +6,7 @@ "properties": { "order_number": { "type": "string", - "description": "order number." + "description": "order number, value must be provided by user." } }, "required": [ "order_number" ] diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json index a6948ac4..8cade6f1 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json @@ -1,5 +1,5 @@ { - "name": "Ordering", + "name": "Order Placement", "description": "Provide types of pizza available, pizza unit price, total cost and place the order.", "createdDateTime": "2023-07-26T02:29:25.123224Z", "updatedDateTime": "2023-07-26T02:29:25.123274Z", diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_order.json similarity index 94% rename from tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json rename to tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_order.json index d5514fba..9fe8278a 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_order.json @@ -1,5 +1,5 @@ { - "name": "place_an_order", + "name": "place_order", "description": "Place an order when user has confirmed the pizza type and quantity.", "parameters": { "type": "object", diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/instructions/instruction.liquid b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/instructions/instruction.liquid index 73b33cc3..5194fc79 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/instructions/instruction.liquid +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/instructions/instruction.liquid @@ -1,10 +1,9 @@ You are now a Pizza Ordering agent, and you can help customers order a pizza according to the user's preferences. -Follow below step to place order: -1: Ask user preferences, call function get_pizza_types to provide the variety of pizza options. -2: Confirm with user the pizza type and quantity. -3: Call function place_an_order to purchase. -4: Ask user how to pay for this order. - -Use below information to help ordering process: * Today is {{current_date}}, the time now is {{current_time}}, day of week is {{current_weekday}}. + +Follow below step to response: +1: Ask user preferences, call function get_pizza_types to provide the variety of pizza options. +2: Call get_pizza_price to tell customer the price per unit, then ask user for the quantity. +3: Confirm the order with total price, call function place_order to place the order. +4: Ask user how to pay for this order. diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/responses/func.get_pizza_price.0.liquid b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/responses/func.get_pizza_price.0.liquid deleted file mode 100644 index 62041ece..00000000 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/responses/func.get_pizza_price.0.liquid +++ /dev/null @@ -1,13 +0,0 @@ -{% assign pizza_type = pizza_type | downcase %} -{% if pizza_type contains "cheese" -%} - The price for a slice of {{pizza_type}} pizza is ${{ cheese_unit_price }}. Would you like to proceed the order? -{%- elsif pizza_type contains "pepperoni" -%} - The price for a slice of {{pizza_type}} pizza is ${{ pepperoni_unit_price }}. Would you like to proceed the order? -{%- elsif pizza_type contains "margherita" -%} - The price for a slice of {{pizza_type}} pizza is ${{ margherita_unit_price }}. Would you like to proceed the order? -{%- else -%} - We don't have {{pizza_type}} pizza, would you like something else? -{%- endif %} -{% if quantity == nil -%} - How many slices would you like to order? -{%- endif %} \ No newline at end of file From 4298e466db17209224cf7690d9e44504e8e1e7b7 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Sun, 27 Apr 2025 16:25:46 -0500 Subject: [PATCH 2/4] remove output format --- .../instructions/instruction.liquid | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid index d49215d1..775de66c 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instructions/instruction.liquid @@ -5,11 +5,7 @@ Follow these steps to handle user request: 2. Determine which agent is suitable to handle this conversation. Try to minimize the routing of human service. 3. Extract and populate agent required arguments, think carefully, leave it as blank object if user didn't provide the specific arguments. 4. You must include all required args for the selected agent, but you must not make up any parameters when there is no exact value provided, those parameters must set value as null if not declared. -{% if routing_mode != 'lazy' %} -5. Response must be in JSON format. -{% else %} 5. If user is greeting, you can call function response_to_user with a greeting message. -{% endif %} {% if routing_requirements and routing_requirements != empty %} [REQUIREMENTS] From 5ff707947089443797c27b44f2fd73210a4de79e Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Fri, 2 May 2025 20:02:37 -0500 Subject: [PATCH 3/4] Optimize InstructLoop --- .../Routing/IRoutingService.cs | 4 +-- .../ConversationService.SendMessage.cs | 26 +++++++-------- .../BotSharp.Core/Routing/RoutingContext.cs | 2 +- .../Routing/RoutingService.InstructLoop.cs | 4 +-- .../BotSharp.Core/Routing/RoutingService.cs | 33 +++++++++++-------- .../Templating/TemplateRender.cs | 2 +- .../BotSharp.Logger/Hooks/VerboseLogHook.cs | 6 ++-- .../Controllers/TwilioInboundController.cs | 24 +++++++++++++- .../Models/ConversationalVoiceRequest.cs | 5 ++- 9 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index d98dd495..97be8874 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -32,7 +32,7 @@ public interface IRoutingService Task InvokeAgent(string agentId, List dialogs); Task InvokeFunction(string name, RoleDialogModel messages); - Task InstructLoop(RoleDialogModel message, List dialogs); + Task InstructLoop(Agent agent, RoleDialogModel message, List dialogs); /// /// Talk to a specific Agent directly, bypassing the Router @@ -40,7 +40,7 @@ public interface IRoutingService /// /// /// - Task InstructDirect(Agent agent, RoleDialogModel message); + Task InstructDirect(Agent agent, RoleDialogModel message, List dialogs); Task GetConversationContent(List dialogs, int maxDialogCount = 100); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 6a844c41..5ec1ae76 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -38,17 +38,6 @@ public partial class ConversationService var routing = _services.GetRequiredService(); routing.Context.SetMessageId(_conversationId, message.MessageId); - // Check the routing mode - var states = _services.GetRequiredService(); - 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)) { @@ -91,11 +80,22 @@ public partial class ConversationService if (agent.Type == AgentType.Routing) { - response = await routing.InstructLoop(message, dialogs); + // Check the routing mode + var states = _services.GetRequiredService(); + var routingMode = states.GetState(StateConst.ROUTING_MODE, "eager"); + 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); + } + + response = await routing.InstructLoop(agent, message, dialogs); } else { - response = await routing.InstructDirect(agent, message); + response = await routing.InstructDirect(agent, message, dialogs); } routing.Context.ResetRecursiveCounter(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 4cba2e2d..1c4ca518 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -294,7 +294,7 @@ public class RoutingContext : IRoutingContext // Set next handling agent for lazy routing mode var states = _services.GetRequiredService(); - var routingMode = states.GetState(StateConst.ROUTING_MODE, "hard"); + var routingMode = states.GetState(StateConst.ROUTING_MODE, "eager"); if (routingMode == "lazy") { var agentId = GetCurrentAgentId(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs index 20aa16b6..76cf5047 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs @@ -7,7 +7,7 @@ namespace BotSharp.Core.Routing; public partial class RoutingService { - public async Task InstructLoop(RoleDialogModel message, List dialogs) + public async Task InstructLoop(Agent agent, RoleDialogModel message, List dialogs) { RoleDialogModel response = default; @@ -15,8 +15,6 @@ public partial class RoutingService var convService = _services.GetRequiredService(); var storage = _services.GetRequiredService(); - _router = await agentService.LoadAgent(message.CurrentAgentId); - var states = _services.GetRequiredService(); var executor = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 253e7b54..3e45fd63 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -26,31 +26,36 @@ public partial class RoutingService : IRoutingService _logger = logger; } - public async Task InstructDirect(Agent agent, RoleDialogModel message) + public async Task InstructDirect(Agent agent, RoleDialogModel message, List dialogs) { var conv = _services.GetRequiredService(); var storage = _services.GetRequiredService(); storage.Append(conv.ConversationId, message); - var dialogs = conv.GetDialogHistory(); + dialogs.Add(message); Context.SetDialogs(dialogs); - var inst = new FunctionCallFromLlm - { - Function = "route_to_agent", - Question = message.Content, - NextActionReason = message.Content, - AgentName = agent.Name, - OriginalAgent = agent.Name, - ExecutingDirectly = true - }; + var routing = _services.GetRequiredService(); + routing.Context.Push(agent.Id, "instruct directly"); + var agentId = routing.Context.GetCurrentAgentId(); - message.Instruction = inst; - var result = await InvokeFunction("route_to_agent", message); + // Update next action agent's name + var agentService = _services.GetRequiredService(); + + if (agent.Disabled) + { + var content = $"This agent ({agent.Name}) is disabled, please install the corresponding plugin ({agent.Plugin.Name}) to activate this agent."; + + message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: content); + dialogs.Add(message); + } + else + { + var ret = await routing.InvokeAgent(agentId, dialogs); + } var response = dialogs.Last(); response.MessageId = message.MessageId; - response.Instruction = inst; return response; } diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index c16a8c4b..24e5d8e2 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -44,7 +44,7 @@ public class TemplateRender : ITemplateRender } else { - _logger.LogWarning(error); + _logger.LogError(error); return template; } } diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/VerboseLogHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/VerboseLogHook.cs index fb3c37a2..391db1a9 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/VerboseLogHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/VerboseLogHook.cs @@ -27,7 +27,7 @@ public class VerboseLogHook : IContentGeneratingHook if (dialog != null) { var log = $"{dialog.Role}: {dialog.Content} [msg_id: {dialog.MessageId}] ==>"; - _logger.LogInformation(log); + _logger.LogDebug(log); } await Task.CompletedTask; @@ -44,7 +44,7 @@ public class VerboseLogHook : IContentGeneratingHook $"[{agent?.Name}]: {message.Indication} {message.FunctionName}({message.FunctionArgs})" : $"[{agent?.Name}]: {message.Content}" + $" <== [msg_id: {message.MessageId}]"; - _logger.LogInformation(tokenStats.Prompt); - _logger.LogInformation(log); + _logger.LogDebug(tokenStats.Prompt); + _logger.LogDebug(log); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index e82908a0..74134a97 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -146,7 +146,7 @@ public class TwilioInboundController : TwilioController AgentId = request.AgentId, Channel = ConversationChannel.Phone, ChannelId = request.CallSid, - Title = $"Incoming phone call from {request.From}", + Title = request.Intent ?? $"Incoming phone call from {request.From}", Tags = [], }; @@ -161,6 +161,15 @@ public class TwilioInboundController : TwilioController new("twilio_call_sid", request.CallSid), }; + var requestStates = ParseStates(request.States); + foreach (var s in requestStates) + { + if (!states.Any(x => x.Key == s.Key)) + { + states.Add(new MessageState(s.Key, s.Value)); + } + } + if (request.InitAudioFile != null) { states.Add(new("init_audio_file", request.InitAudioFile)); @@ -173,7 +182,20 @@ public class TwilioInboundController : TwilioController { states.Add(new(StateConst.ROUTING_MODE, agent.Mode)); } + convService.SetConversationId(conversation.Id, states); + + if (!string.IsNullOrEmpty(request.Intent)) + { + var storage = _services.GetRequiredService(); + + storage.Append(conversation.Id, new RoleDialogModel(AgentRole.User, request.Intent) + { + CurrentAgentId = conversation.Id, + CreatedAt = DateTime.UtcNow + }); + } + convService.SaveStates(); // reload agent rendering with states diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs index df08df0a..344ddc06 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Models/ConversationalVoiceRequest.cs @@ -17,7 +17,10 @@ public class ConversationalVoiceRequest : VoiceRequest public int AIResponseWaitTime { get; set; } = 0; public string? AIResponseErrorMessage { get; set; } = string.Empty; - public string Intent { get; set; } = string.Empty; + /// + /// Initial intent when incoming call connected + /// + public string? Intent { get; set; } [FromQuery(Name = "init-audio-file")] public string? InitAudioFile { get; set; } From 97115c64969bea5b48f5a9c30863c5aeca48ec7b Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Mon, 5 May 2025 10:10:32 -0500 Subject: [PATCH 4/4] Fix InstructLoop --- .../BotSharp.Core/Routing/RoutingService.InstructLoop.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs index 76cf5047..364e7198 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs @@ -15,6 +15,8 @@ public partial class RoutingService var convService = _services.GetRequiredService(); var storage = _services.GetRequiredService(); + _router = await agentService.GetAgent(message.CurrentAgentId); + var states = _services.GetRequiredService(); var executor = _services.GetRequiredService();