diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs index 6e5963e8..8d950258 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Routing.Models; +using System.Text.Json; namespace BotSharp.Abstraction.Functions.Models; @@ -7,14 +8,27 @@ public class FunctionCallFromLlm [JsonPropertyName("function")] public string Function { get; set; } = string.Empty; - [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; + [JsonPropertyName("route")] + public RoutingArgs Route { get; set; } = new RoutingArgs(); - [JsonPropertyName("parameters")] - public RetrievalArgs Parameters { get; set; } = new RetrievalArgs(); + [JsonPropertyName("question")] + public string? Question { get; set; } + + [JsonPropertyName("answer")] + public string? Answer { get; set; } + + [JsonPropertyName("args")] + public JsonDocument Arguments { get; set; } = JsonDocument.Parse("{}"); public override string ToString() { - return $"{Function} ({Reason}) {Parameters}"; + if (string.IsNullOrEmpty(Answer)) + { + return $"[{Function} {Route} {JsonSerializer.Serialize(Arguments)}]: {Question}"; + } + else + { + return $"[{Function} {Route} {JsonSerializer.Serialize(Arguments)}]: {Question} => {Answer}"; + } } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs new file mode 100644 index 00000000..60354ac7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingHandler.cs @@ -0,0 +1,17 @@ +using BotSharp.Abstraction.Functions.Models; + +namespace BotSharp.Abstraction.Routing; + +public interface IRoutingHandler +{ + string Name { get; } + string Description { get; } + bool IsReasoning { get; } + bool RequireAgent { get; } + List Parameters { get; } + void SetRouter(Agent router); + void SetDialogs(List dialogs); + Task GetNextInstructionFromReasoner(string prompt); + Task GetResponseFromReasoner(); + Task Handle(FunctionCallFromLlm inst); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs deleted file mode 100644 index 51f2f777..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json; - -namespace BotSharp.Abstraction.Routing.Models; - -public class RetrievalArgs : RoutingArgs -{ - [JsonPropertyName("question")] - public string Question { get; set; } - - [JsonPropertyName("answer")] - public string Answer { get; set; } - - [JsonPropertyName("args")] - public JsonDocument Arguments { get; set; } = JsonDocument.Parse("{}"); - - public override string ToString() - { - if (string.IsNullOrEmpty(Answer)) - { - return $"[{AgentName}]: ({JsonSerializer.Serialize(Arguments)}) {Question}"; - } - else - { - return $"[{AgentName}]: ({JsonSerializer.Serialize(Arguments)}) {Question} => {Answer}"; - } - } -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs index 474e26da..2204d375 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs @@ -1,14 +1,18 @@ -using System.Text.Json.Serialization; - namespace BotSharp.Abstraction.Routing.Models; public class RoutingArgs { + [JsonPropertyName("user_goal")] + public string UserGoal { get; set; } = string.Empty; + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; + [JsonPropertyName("agent_name")] public string AgentName { get; set; } = string.Empty; public override string ToString() { - return AgentName; + return string.IsNullOrEmpty(AgentName) ? "" : $""; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingHandlerDef.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingHandlerDef.cs new file mode 100644 index 00000000..8b792287 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingHandlerDef.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Routing.Models; + +public class RoutingHandlerDef +{ + public string Name { get; set; } + public string Description { get; set; } + public List Parameters { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index e829cff9..b93316a1 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -87,9 +87,10 @@ + - + diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index 89e1c57f..082f07d2 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using BotSharp.Abstraction.Templating; using BotSharp.Core.Instructs; using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Routing; +using BotSharp.Core.Routing.Handlers; namespace BotSharp.Core; @@ -56,7 +57,17 @@ public static class BotSharpServiceCollectionExtensions // Register function callback services.AddScoped(); + // Register routing and handlers services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); if (myDatabaseSettings.Default == "FileRepository") { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs index 8d63fc64..71608749 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs @@ -27,7 +27,7 @@ public partial class ConversationService text = latestResponse.Content.Split("=>").Last(); } - await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, text) + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, text) { CurrentAgentId = agent.Id }, onMessageReceived); @@ -37,7 +37,7 @@ public partial class ConversationService var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg => { - await HandleAssistantMessage(agent, msg, onMessageReceived); + await HandleAssistantMessage(msg, onMessageReceived); }, async fn => { var preAgentId = agent.Id; @@ -47,7 +47,7 @@ public partial class ConversationService // Function executed has exception if (fn.ExecutionResult == null) { - await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, fn.Content) + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, fn.Content) { CurrentAgentId = fn.CurrentAgentId }, onMessageReceived); @@ -56,7 +56,7 @@ public partial class ConversationService } else if (fn.StopCompletion) { - await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, fn.Content) + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, fn.Content) { CurrentAgentId = fn.CurrentAgentId, ExecutionData = fn.ExecutionData, @@ -95,7 +95,7 @@ public partial class ConversationService var response = await templateService.RenderFunctionResponse(agent.Id, fn); if (!string.IsNullOrEmpty(response)) { - await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, response) + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, response) { CurrentAgentId = agent.Id }, onMessageReceived); @@ -124,7 +124,7 @@ public partial class ConversationService return result; } - private async Task HandleAssistantMessage(Agent agent, RoleDialogModel message, Func onMessageReceived) + private async Task HandleAssistantMessage(RoleDialogModel message, Func onMessageReceived) { var hooks = _services.GetServices().ToList(); @@ -134,7 +134,9 @@ public partial class ConversationService await hook.AfterCompletion(message); } - _logger.LogInformation($"[{agent.Name}] {message.Role}: {message.Content}"); + var agent = await _services.GetRequiredService().GetAgent(message.CurrentAgentId); + + _logger.LogInformation($"[{agent?.Name ?? "Router"}] {message.Role}: {message.Content}"); await onMessageReceived(message); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 6d3293c4..a969c0f9 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -57,40 +57,19 @@ public partial class ConversationService var routing = _services.GetRequiredService(); var reasonedContext = await routing.Enter(agent, wholeDialogs); - if (reasonedContext.FunctionName == "interrupt_task_execution") + if (reasonedContext.StopCompletion) { - await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, reasonedContext.Content) - { - CurrentAgentId = agent.Id - }, onMessageReceived); - + await HandleAssistantMessage(reasonedContext, onMessageReceived); return true; } - else if (reasonedContext.FunctionName == "response_to_user") - { - await HandleAssistantMessage(agent, new RoleDialogModel(AgentRole.Assistant, reasonedContext.Content) - { - CurrentAgentId = agent.Id - }, onMessageReceived); - return true; - } - else if (reasonedContext.FunctionName == "continue_execute_task") + // Switch agent + if (reasonedContext.CurrentAgentId != agent.Id) { - if (reasonedContext.CurrentAgentId != agent.Id) - { - agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId); - } - } - else if (reasonedContext.FunctionName == "route_to_agent") - { - if (reasonedContext.CurrentAgentId != agent.Id) - { - agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId); - } + agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId); } - routing.Dialogs.ForEach(x => + routing.Dialogs.ForEach(x => { wholeDialogs.Add(x); if (x.Content != null) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 3357549a..e9cda842 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -46,7 +46,7 @@ public class ConversationStorage : IConversationStorage else { var routingSetting = _services.GetRequiredService(); - var agentName = routingSetting.RouterId == agentId ? "Router" : db.Agents.First(x => x.Id == agentId).Name; + var agentName = routingSetting.RouterId == agentId ? routingSetting.RouterName : db.Agents.First(x => x.Id == agentId).Name; sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agentId}|{agentName}|"); var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs new file mode 100644 index 00000000..b8e24d15 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs @@ -0,0 +1,43 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Repositories; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +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 + { + "1. agent_name: the name of the agent", + "2. args: required parameters extracted from question", + "3. reason: why continue to execute current task" + }; + + public bool IsReasoning => true; + + public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + var routing = _services.GetRequiredService(); + var db = _services.GetRequiredService(); + var record = db.Agents.First(x => x.Name.ToLower() == inst.Route.AgentName.ToLower()); + + var result = new RoleDialogModel(AgentRole.Function, inst.Question) + { + FunctionName = inst.Function, + FunctionArgs = JsonSerializer.Serialize(inst.Arguments), + CurrentAgentId = record.Id + }; + + return result; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs new file mode 100644 index 00000000..d1343f87 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ConversationEndRoutingHandler.cs @@ -0,0 +1,28 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +namespace BotSharp.Core.Routing.Handlers; + +public class ConversationEndRoutingHandler : RoutingHandlerBase, IRoutingHandler +{ + public string Name => "conversation_end"; + + public string Description => "Call this function when user wants to end this conversation or all tasks have been completed."; + + public List Parameters => new List + { + }; + + public bool IsReasoning => false; + + public ConversationEndRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public Task Handle(FunctionCallFromLlm inst) + { + throw new NotImplementedException(); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/GetNextInstructionRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/GetNextInstructionRoutingHandler.cs new file mode 100644 index 00000000..115cf86d --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/GetNextInstructionRoutingHandler.cs @@ -0,0 +1,26 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +namespace BotSharp.Core.Routing.Handlers; + +public class GetNextInstructionRoutingHandler : RoutingHandlerBase, IRoutingHandler +{ + public string Name => "get_next_instruction"; + + public string Description => ""; + + public List Parameters => new List { }; + + public bool IsReasoning => false; + + public GetNextInstructionRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + return null; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs new file mode 100644 index 00000000..10601361 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs @@ -0,0 +1,36 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +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 + { + "1. reason: the reason why the request is interrupted", + "2. answer: the content response to user" + }; + + public bool IsReasoning => true; + + public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + var result = new RoleDialogModel(AgentRole.User, inst.Route.Reason) + { + FunctionName = inst.Function, + StopCompletion = true + }; + + return result; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs new file mode 100644 index 00000000..d5592a78 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ResponseToUserRoutingHandler.cs @@ -0,0 +1,35 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +namespace BotSharp.Core.Routing.Handlers; + +public class ResponseToUserRoutingHandler : RoutingHandlerBase, IRoutingHandler +{ + public string Name => "response_to_user"; + + public string Description => "You know how to response according to the context, don't need to ask specific agent."; + + public List Parameters => new List + { + "1. answer: the content of response", + "2. reason: why response to user" + }; + + public bool IsReasoning => false; + + public ResponseToUserRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + var result = new RoleDialogModel(AgentRole.User, inst.Answer) + { + FunctionName = inst.Function, + StopCompletion = true + }; + return result; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs new file mode 100644 index 00000000..88b3917f --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs @@ -0,0 +1,69 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Repositories; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +namespace BotSharp.Core.Routing.Handlers; + +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 + { + "1. agent_name: the name of the agent", + "2. question: the question you will ask the agent to get the necessary data", + "3. reason: why retrieve data", + "4. args: required parameters extracted from question and hand over to the next agent. The args should be in JSON format" + }; + + public bool IsReasoning => true; + + public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + if (string.IsNullOrEmpty(inst.Route.AgentName)) + { + inst = await GetNextInstructionFromReasoner($"What's the next step? your response must have agent name."); + } + + // Retrieve information from specific agent + var db = _services.GetRequiredService(); + var record = db.Agents.First(x => x.Name.ToLower() == inst.Route.AgentName.ToLower()); + var response = await InvokeAgent(record.Id, new List + { + new RoleDialogModel(AgentRole.User, inst.Question) + }); + + inst.Answer = response.Content; + + /*_dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question) + { + CurrentAgentId = record.Id + });*/ + + _router.Instruction += $"\r\n{AgentRole.Assistant}: {inst.Question}"; + + /*_dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer) + { + FunctionName = inst.Function, + FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), + ExecutionResult = inst.Parameters.Answer, + ExecutionData = response.ExecutionData, + CurrentAgentId = record.Id + });*/ + + _router.Instruction += $"\r\n{AgentRole.Function}: {response.Content}"; + + // Got the response from agent, then send to reasoner again to make the decision + inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?"); + + return null; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs new file mode 100644 index 00000000..9e6dc921 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs @@ -0,0 +1,61 @@ +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Models; +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 agent."; + + public List Parameters => new List + { + "1. agent_name: the name of the agent", + "2. reason: why route to this agent", + "3. args: parameters extracted from context", + "4. answer: if you know how to response without asking to other agent", + "5. goal: user's original goal" + }; + + public bool IsReasoning => false; + + public RouteToAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + if (string.IsNullOrEmpty(inst.Route.AgentName)) + { + inst = await GetNextInstructionFromReasoner($"What's the next step? your response must have agent name."); + } + + var function = _services.GetServices().FirstOrDefault(x => x.Name == inst.Function); + var message = new RoleDialogModel(AgentRole.Function, inst.Question) + { + FunctionName = inst.Function, + FunctionArgs = JsonSerializer.Serialize(new RoutingArgs + { + AgentName = inst.Route.AgentName + }), + }; + + var ret = await function.Execute(message); + + var result = await InvokeAgent(message.CurrentAgentId, _dialogs); + result.ExecutionData = result.ExecutionData ?? message.ExecutionData; + + if (result.Role == AgentRole.Function && !result.StopCompletion) + { + _dialogs.Add(result); + result = await InvokeAgent(message.CurrentAgentId, _dialogs); + } + + return result; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RoutingHandlerBase.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RoutingHandlerBase.cs new file mode 100644 index 00000000..78fc8d5d --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RoutingHandlerBase.cs @@ -0,0 +1,155 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing.Models; +using BotSharp.Abstraction.Routing.Settings; +using System.Drawing; + +namespace BotSharp.Core.Routing.Handlers; + +public abstract class RoutingHandlerBase +{ + protected Agent _router; + protected readonly IServiceProvider _services; + protected readonly ILogger _logger; + protected RoutingSettings _settings; + protected List _dialogs; + public virtual bool RequireAgent => true; + + public RoutingHandlerBase(IServiceProvider services, + ILogger logger, + RoutingSettings settings) + { + _services = services; + _logger = logger; + _settings = settings; + } + + public void SetRouter(Agent router) + { + _router = router; + } + + public void SetDialogs(List dialogs) + { + _dialogs = dialogs; + } + + public async Task GetNextInstructionFromReasoner(string prompt) + { + var responseFormat = JsonSerializer.Serialize(new FunctionCallFromLlm()); + var content = $"{prompt} Response must be in JSON format {responseFormat}."; + + var chatCompletion = CompletionProvider.GetChatCompletion(_services, + provider: _settings.Provider, + model: _settings.Model); + + RoleDialogModel response = null; + await chatCompletion.GetChatCompletionsAsync(_router, new List + { + new RoleDialogModel(AgentRole.User, content) + }, async msg + => response = msg, fn + => Task.CompletedTask); + + FunctionCallFromLlm args = new FunctionCallFromLlm(); + try + { + _logger.LogInformation(response.Content); + args = JsonSerializer.Deserialize(response.Content); + } + catch (Exception ex) + { + _logger.LogError($"{ex.Message}: {response.Content}"); + args.Function = "response_to_user"; + args.Answer = ex.Message; + args.Route.AgentName = ""; + } + + if (args.Arguments != null) + { + SaveStateByArgs(args.Arguments); + } + + args.Function = args.Function.Split('.').Last(); + +#if DEBUG + Console.WriteLine($"*** Next Instruction *** {args}", Color.Green); +#else + _logger.LogInformation($"*** Next Instruction *** {args}"); +#endif + + return args; + } + + public async Task GetResponseFromReasoner() + { + var wholeDialogs = new List + { + new RoleDialogModel(AgentRole.User, $"How to response to user?") + }; + + var chatCompletion = CompletionProvider.GetChatCompletion(_services, + provider: _settings.Provider, + model: _settings.Model); + + RoleDialogModel response = null; + await chatCompletion.GetChatCompletionsAsync(_router, wholeDialogs, async msg + => response = msg, fn + => Task.CompletedTask); + + return response; + } + + protected async Task InvokeAgent(string agentId, List wholeDialogs) + { + var agentService = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(agentId); + + var chatCompletion = CompletionProvider.GetChatCompletion(_services); + + RoleDialogModel response = null; + await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, + async msg => + { + response = msg; + }, async fn => + { + // execute function + // Save states + SaveStateByArgs(JsonSerializer.Deserialize(fn.FunctionArgs)); + + var conversationService = _services.GetRequiredService(); + // Call functions + await conversationService.CallFunctions(fn); + + response = fn; + + if (string.IsNullOrEmpty(response.Content)) + { + response.Content = fn.ExecutionResult; + } + }); + + return response; + } + + protected void SaveStateByArgs(JsonDocument args) + { + if (args == null) + { + return; + } + + var stateService = _services.GetRequiredService(); + if (args.RootElement is JsonElement root) + { + foreach (JsonProperty property in root.EnumerateObject()) + { + if (!string.IsNullOrEmpty(property.Value.ToString())) + { + stateService.SetState(property.Name, property.Value); + } + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs new file mode 100644 index 00000000..50b14d2b --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TaskEndRoutingHandler.cs @@ -0,0 +1,29 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +namespace BotSharp.Core.Routing.Handlers; + +public class TaskEndRoutingHandler : RoutingHandlerBase, IRoutingHandler +{ + public string Name => "task_end"; + + public string Description => "Call this function when current task is completed."; + + public List Parameters => new List + { + "1. abandoned_arguments: the arguments next task can't reuse" + }; + + public bool IsReasoning => true; + + public TaskEndRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public Task Handle(FunctionCallFromLlm inst) + { + throw new NotImplementedException(); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/TransferToCsrRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TransferToCsrRoutingHandler.cs new file mode 100644 index 00000000..6628cd9f --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/TransferToCsrRoutingHandler.cs @@ -0,0 +1,34 @@ +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Routing.Settings; + +namespace BotSharp.Core.Routing.Handlers; + +public class TransferToCsrRoutingHandler : RoutingHandlerBase, IRoutingHandler +{ + public string Name => "transfer_to_csr"; + + public string Description => "Reach out to a real customer representative to help."; + + public List Parameters => new List + { + }; + + public bool IsReasoning => false; + + public TransferToCsrRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) + : base(services, logger, settings) + { + } + + public async Task Handle(FunctionCallFromLlm inst) + { + var result = new RoleDialogModel(AgentRole.User, "I'm transferring to a customer representative, waiting a moment please.") + { + CurrentAgentId = _settings.RouterId, + FunctionName = inst.Function, + StopCompletion = true + }; + return result; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/PromptConst.cs b/src/Infrastructure/BotSharp.Core/Routing/PromptConst.cs index 18c10cd9..33466079 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/PromptConst.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/PromptConst.cs @@ -3,70 +3,31 @@ namespace BotSharp.Core.Routing; public class PromptConst { public const string ROUTER_PROMPT = @" -You're a Router with reasoning, you can dispatch request to different agent to complete the task. +You're a Router with reasoning, you can dispatch request to different agent to achieve user's goal. -### Agents: +### +Router can decide which of below agents can handle user's request: {% for agent in routing_records %} * {{ agent.name }} {{ agent.description }} -{% if agent.required_fields != empty -%}Required information: {{ agent.required_fields }}.{%- endif %} +{% if agent.required_fields != empty -%} +Required: {% for field in agent.required_fields %}{{ field }},{% endfor %} +{%- endif %} {% endfor %} -### Functions -{% if enable_reasoning == false -%} -* route_to_agent -Route request to appropriate agent. +### +Agent can utilize below functions: +{% for fn in routing_handlers %} +* {{ fn.name }} +{{ fn.description }} +{% if fn.parameters != empty %} Parameters: -1. agent_name: the name of the agent; -2. reason: why route to this agent; -3. args: parameters extracted from context; -{%- endif %} - -* task_end -Call this function when current task is completed. -Parameters: -1. abandoned_arguments: the arguments next task can't reuse; - -* conversation_end -Call this function when user wants to end this conversation or all tasks have been completed. - -* transfer_to_csr -Reach out to a real customer representative to help. - -{{ reasoning_functions }} - -### Your response must meet below requirements strictly -{% if enable_reasoning == false %} -* If you can find an appropriate Agent, you must call function route_to_agent with required arguments. +{% for arg in fn.parameters -%} +{{ arg }}; +{%- endfor %} {% endif %} +{% endfor %} -### Conversation context:"; - - public const string REASONING_FUNCTIONS = @" -* retrieve_data_from_agent -Retrieve data from appropriate agent. -Parameters: -1. agent_name: the name of the agent; -2. question: the question you will ask the agent to get the necessary data; -3. reason: why retrieve data; -4. args: required parameters extracted from question and hand over to the next agent. The args should be in JSON format; - -* continue_execute_task -Continue to execute user's request without further information retrival. -Parameters: -1. agent_name: the name of the agent; -2. args: required parameters extracted from question; -3. reason: why continue to execute current task; - -* interrupt_task_execution -Can't continue user's request becauase the requirements are not met. -Parameters: -1. reason: the reason why the request is interrupted; -2. answer: the content response to user; - -* response_to_user -You have already known the answer according the dialogs. -Parameters: -1. answer: the response of user's request; -2. reason: why response to user;"; +### +Conversation context:"; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs index 21804b75..0c16dc09 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RouteToAgentFn.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Routing.Models; +using System.Drawing; namespace BotSharp.Core.Routing; @@ -115,6 +116,12 @@ public class RouteToAgentFn : IFunctionCallback // Add redirected agent message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name); agentId = routingRule.RedirectTo; + var logger = _services.GetRequiredService>(); +#if DEBUG + Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow); +#else + logger.LogInformation($"*** Routing redirect to {record.Name.ToUpper()} ***"); +#endif } else { diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 4eae4709..1befba80 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,12 +1,10 @@ using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions; -using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Routing.Settings; using BotSharp.Abstraction.Templating; -using System.IO; +using System.Runtime.InteropServices; namespace BotSharp.Core.Routing; @@ -27,196 +25,56 @@ public class RoutingService : IRoutingService _logger = logger; } - public async Task Enter(Agent router, List whileDialogs) + public async Task Enter(Agent router, List wholeDialogs) { _dialogs = new List(); - RoleDialogModel result = new RoleDialogModel(AgentRole.Assistant, "not handled"); + var result = new RoleDialogModel(AgentRole.Assistant, "Can you repeat your request again?") + { + CurrentAgentId = router.Id + }; - foreach (var dialog in whileDialogs.TakeLast(20)) + var message = wholeDialogs.Last().Content; + foreach (var dialog in wholeDialogs.TakeLast(20)) { router.Instruction += $"\r\n{dialog.Role}: {dialog.Content}"; } - var inst = await GetNextInstructionFromReasoner($"What's the next step to make user's original goal?", router); + var handlers = _services.GetServices(); + + var handler = handlers.FirstOrDefault(x => x.Name == "get_next_instruction"); + handler.SetRouter(router); + handler.SetDialogs(wholeDialogs); + int loopCount = 0; - while (loopCount < 3) + while (!result.StopCompletion && loopCount < 5) { loopCount++; - if (inst.Function == "continue_execute_task") + + var inst = await handler.GetNextInstructionFromReasoner($"What's the next step to achieve user's goal?"); + inst.Question = inst.Question ?? message; + + handler = handlers.FirstOrDefault(x => x.Name == inst.Function); + if (handler == null) { - var routing = _services.GetRequiredService(); - var db = _services.GetRequiredService(); - var record = db.Agents.First(x => x.Name.ToLower() == inst.Parameters.AgentName.ToLower()); - - result = new RoleDialogModel(AgentRole.Function, inst.Parameters.Question) - { - FunctionName = inst.Function, - FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), - CurrentAgentId = record.Id, - }; - break; + handler = handlers.FirstOrDefault(x => x.Name == "get_next_instruction"); + router.Instruction += $"\r\n{AgentRole.System}: the function must be one of [{string.Join(",", GetHandlers().Select(x => x.Name))}]."; + continue; } - // Compatible with previous Router, can be removed in the future. - else if (inst.Function == "route_to_agent") - { - // If the agent name is empty, fallback to router - if (string.IsNullOrEmpty(inst.Parameters.AgentName)) - { - result = new RoleDialogModel(AgentRole.Function, inst.Reason) - { - FunctionName = inst.Function, - FunctionArgs = JsonSerializer.Serialize(new RoutingArgs - { - AgentName = inst.Parameters.AgentName - }), - CurrentAgentId = router.Id - }; - break; - } + handler.SetRouter(router); + handler.SetDialogs(wholeDialogs); - var function = _services.GetServices().FirstOrDefault(x => x.Name == inst.Function); - result = new RoleDialogModel(AgentRole.Function, inst.Reason) - { - FunctionName = inst.Function, - FunctionArgs = JsonSerializer.Serialize(new RoutingArgs - { - AgentName = inst.Parameters.AgentName - }), - }; - var ret = await function.Execute(result); - break; - } - else if (inst.Function == "interrupt_task_execution") - { - result = new RoleDialogModel(AgentRole.User, inst.Reason) - { - FunctionName = inst.Function - }; - break; - } - else if (inst.Function == "response_to_user") - { - result = new RoleDialogModel(AgentRole.User, inst.Parameters.Answer) - { - FunctionName = inst.Function - }; - break; - } - else if (inst.Function == "retrieve_data_from_agent") - { - // Retrieve information from specific agent - var db = _services.GetRequiredService(); - var record = db.Agents.First(x => x.Name.ToLower() == inst.Parameters.AgentName.ToLower()); - var response = await RetrieveDataFromAgent(record.Id, new List - { - new RoleDialogModel(AgentRole.User, inst.Parameters.Question) - }); + result = await handler.Handle(inst); - inst.Parameters.Answer = response.Content; + message = result.Content.Replace("\r\n", " "); + router.Instruction += $"\r\n{result.Role}: {message}"; - _dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question) - { - CurrentAgentId = record.Id - }); - - router.Instruction += $"\r\n{AgentRole.Assistant}: {inst.Parameters.Question}"; - - _dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer) - { - FunctionName = inst.Function, - FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), - ExecutionResult = inst.Parameters.Answer, - ExecutionData = response.ExecutionData, - CurrentAgentId = record.Id - }); - - router.Instruction += $"\r\n{AgentRole.Function}: {response.Content}"; - - // Got the response from agent, then send to reasoner again to make the decision - inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?", router); - } + result.StopCompletion = !_settings.EnableReasoning; } return result; } - private async Task GetNextInstructionFromReasoner(string prompt, Agent reasoner) - { - var responseFormat = JsonSerializer.Serialize(new FunctionCallFromLlm()); - var wholeDialogs = new List - { - new RoleDialogModel(AgentRole.User, $"{prompt} Response in JSON format {responseFormat}") - }; - var chatCompletion = CompletionProvider.GetChatCompletion(_services, - provider: _settings.Provider, - model: _settings.Model); - - RoleDialogModel response = null; - await chatCompletion.GetChatCompletionsAsync(reasoner, wholeDialogs, async msg - => response = msg, fn - => Task.CompletedTask); - - var args = JsonSerializer.Deserialize(response.Content); - - if (args.Parameters.Arguments != null) - { - SaveStateByArgs(args.Parameters.Arguments); - } - - args.Function = args.Function.Split('.').Last(); - args.Parameters.AgentName = args.Parameters.AgentName.Split(':').Last().Trim(); - - _logger.LogInformation($"*** Next Instruction *** {args}"); - - return args; - } - - private async Task RetrieveDataFromAgent(string agentId, List wholeDialogs) - { - var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(agentId); - - var chatCompletion = CompletionProvider.GetChatCompletion(_services); - - RoleDialogModel response = null; - await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg - => response = msg, async fn - => - { - // execute function - // Save states - SaveStateByArgs(JsonSerializer.Deserialize(fn.FunctionArgs)); - - var conversationService = _services.GetRequiredService(); - // Call functions - await conversationService.CallFunctions(fn); - - response = fn; - response.Content = fn.ExecutionResult; - }); - return response; - } - - private void SaveStateByArgs(JsonDocument args) - { - if (args == null) - { - return; - } - - var stateService = _services.GetRequiredService(); - if (args.RootElement is JsonElement root) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (!string.IsNullOrEmpty(property.Value.ToString())) - { - stateService.SetState(property.Name, property.Value); - } - } - } - } public Agent LoadRouter() { @@ -241,15 +99,24 @@ public class RoutingService : IRoutingService .ToArray() }).ToArray(); - dict["enable_reasoning"] = _settings.EnableReasoning; - if (_settings.EnableReasoning) - { - dict["reasoning_functions"] = PromptConst.REASONING_FUNCTIONS; - } + dict["routing_handlers"] = GetHandlers(); var render = _services.GetRequiredService(); router.Instruction = render.Render(PromptConst.ROUTER_PROMPT, dict); return router; } + + private List GetHandlers() + { + return _services.GetServices() + .Where(x => x.IsReasoning == _settings.EnableReasoning) + .Where(x => !string.IsNullOrEmpty(x.Description)) + .Select(x => new RoutingHandlerDef + { + Name = x.Name, + Description = x.Description, + Parameters = x.Parameters + }).ToList(); + } } diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index 240783ff..6f08efaf 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -18,6 +18,7 @@ public class TemplateRender : ITemplateRender _options = new TemplateOptions(); _options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.SnakeCase; _options.MemberAccessStrategy.Register(); + _options.MemberAccessStrategy.Register(); } public string Render(string template, Dictionary dict) diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index 41e10730..4571da54 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -22,4 +22,5 @@ global using BotSharp.Core.Agents.Services; global using BotSharp.Core.Conversations.Services; global using BotSharp.Core.Infrastructures; global using BotSharp.Core.Users.Services; -global using Aspects.Cache; \ No newline at end of file +global using Aspects.Cache; +global using Console = Colorful.Console; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 3df4a110..913642dd 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -70,6 +70,8 @@ public class ConversationController : ControllerBase, IApiAdapter }); response.Text = string.Join("\r\n", stackMsg.Select(x => x.Content)); + response.Data = response.Data ?? stackMsg.Last().ExecutionData; + return response; } }