diff --git a/Directory.Build.props b/Directory.Build.props index 249103d7..a0e7305b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ 10.0 ..\..\..\packages - 0.12.3 + 0.13.0 true \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs index 5d9b9881..968ae667 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -11,4 +11,9 @@ public class FunctionCallFromLlm [JsonPropertyName("parameters")] public RetrievalArgs Parameters { get; set; } + + public override string ToString() + { + return $"{Function}: {Parameters}"; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index 8cf7f5b7..08234983 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -2,6 +2,6 @@ namespace BotSharp.Abstraction.Routing; public interface IRoutingService { - Task DeleteRoutingItems(); - Task DeleteRoutingProfiles(); + List Dialogs { get; } + Task Enter(Agent agent, List whileDialogs); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs index e0b3f9a1..bc173c33 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs @@ -16,4 +16,9 @@ public class RetrievalArgs : RoutingArgs [JsonPropertyName("args")] public JsonDocument Arguments { get; set; } + + public override string ToString() + { + return $"{AgentName} {Question} ({JsonSerializer.Serialize(Arguments)}) => {Answer} ({Reason})"; + } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index d635d1c0..a0133eac 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -62,6 +62,7 @@ public static class BotSharpServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); if (myDatabaseSettings.Default == "FileRepository") { diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 1c297391..366d13d2 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -1,6 +1,6 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing; namespace BotSharp.Core.Conversations.Services; @@ -52,10 +52,10 @@ public partial class ConversationService // reasoning var settings = _services.GetRequiredService(); - if (settings.ReasonerId == agent.Id) + if (settings.RouterId == agent.Id) { - var simulator = _services.GetRequiredService(); - var reasonedContext = await simulator.Enter(agent, wholeDialogs); + var routing = _services.GetRequiredService(); + var reasonedContext = await routing.Enter(agent, wholeDialogs); if (reasonedContext.FunctionName == "interrupt_task_execution") { @@ -83,7 +83,7 @@ public partial class ConversationService } } - simulator.Dialogs.ForEach(x => + routing.Dialogs.ForEach(x => { wholeDialogs.Add(x); if (x.Content != null) diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs new file mode 100644 index 00000000..f0566827 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -0,0 +1,183 @@ +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; + +namespace BotSharp.Core.Routing; + +public class RoutingService : IRoutingService +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private List _dialogs; + public List Dialogs => _dialogs; + + public RoutingService(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Enter(Agent agent, List whileDialogs) + { + _dialogs = new List(); + RoleDialogModel result = new RoleDialogModel(AgentRole.Assistant, "not handled"); + + foreach (var dialog in whileDialogs.TakeLast(10)) + { + agent.Instruction += $"\r\n{dialog.Role}: {dialog.Content}"; + } + + var inst = await GetNextInstructionFromReasoner(agent); + int loopCount = 0; + while (loopCount < 3) + { + loopCount++; + if (inst.Function == "continue_execute_task") + { + var router = _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; + } + // Compatible with previous Router, can be removed in the future. + else if (inst.Function == "route_to_agent") + { + var function = _services.GetServices().FirstOrDefault(x => x.Name == inst.Function); + result = new RoleDialogModel(AgentRole.Function, inst.Parameters.Question) + { + 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.Parameters.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) + }); + + response.Content += $"\r\nDo you want to continue current task?"; + _dialogs.Add(new RoleDialogModel(AgentRole.Function, $"{record.Name}: {response.Content}") + { + FunctionName = inst.Function, + FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), + ExecutionResult = response.Content, + CurrentAgentId = record.Id + }); + + agent.Instruction += $"\r\n{record.Name}: {response.Content}"; + + // Got the response from agent, then send to reasoner again to make the decision + inst = await GetNextInstructionFromReasoner(agent); + } + } + + return result; + } + + private async Task GetNextInstructionFromReasoner(Agent reasoner) + { + var wholeDialogs = new List + { + new RoleDialogModel(AgentRole.User, @"What's the next step? Response in JSON format with ""function"" and ""parameters"".") + }; + + var chatCompletion = CompletionProvider.GetChatCompletion(_services); + + 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(); + + 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); + } + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs b/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs index 7e3cbf3e..b13eb5bb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs @@ -1,9 +1,8 @@ -using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Repositories; +using BotSharp.Abstraction.Routing.Models; namespace BotSharp.Core.Routing; @@ -33,6 +32,11 @@ public class Simulator } var response = await SendMessageToReasoner(agent); + if (response.Role == AgentRole.Function) + { + + } + var args = JsonSerializer.Deserialize(response.Content); response.FunctionName = args.Function; @@ -42,7 +46,7 @@ public class Simulator var router = _services.GetRequiredService(); var db = _services.GetRequiredService(); - var record = db.Agents.First(x => x.Name.ToLower() == args.Parameters.AgentName); + var record = db.Agents.First(x => x.Name.ToLower() == args.Parameters.AgentName.ToLower()); response.CurrentAgentId = record.Id; } else if (args.Function == "interrupt_task_execution") @@ -75,7 +79,7 @@ public class Simulator var args = JsonSerializer.Deserialize(response.Content); - if (args.Function == "retrieve_data_from_agent") + if (args.Parameters.Arguments != null) { SaveStateByArgs(args.Parameters.Arguments); } @@ -84,10 +88,28 @@ public class Simulator return response; } + if (args.Function == "route_to_agent") + { + var function = _services.GetServices().FirstOrDefault(x => x.Name == args.Function); + var message = new RoleDialogModel(AgentRole.Function, args.Parameters.Question) + { + FunctionName = args.Function, + FunctionArgs = JsonSerializer.Serialize(new RoutingArgs + { + AgentName = args.Parameters.AgentName + }), + }; + var ret = await function.Execute(message); + if (ret) + { + return message; + } + } + // Retrieve information from specific agent var router = _services.GetRequiredService(); var db = _services.GetRequiredService(); - var record = db.Agents.First(x => x.Name.ToLower() == args.Parameters.AgentName); + var record = db.Agents.First(x => x.Name.ToLower() == args.Parameters.AgentName.ToLower()); response = await SendMessageToAgent(record.Id, new List { new RoleDialogModel(AgentRole.User, args.Parameters.Question) diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetBakingTimeFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetDeliveryTimeFn.cs similarity index 70% rename from tests/BotSharp.Plugin.PizzaBot/Functions/GetBakingTimeFn.cs rename to tests/BotSharp.Plugin.PizzaBot/Functions/GetDeliveryTimeFn.cs index 0ebc56e7..db3179af 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/GetBakingTimeFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/GetDeliveryTimeFn.cs @@ -2,9 +2,9 @@ using BotSharp.Abstraction.Conversations.Models; namespace BotSharp.Plugin.PizzaBot.Functions; -public class GetBakingTimeFn : IFunctionCallback +public class GetDeliveryTimeFn : IFunctionCallback { - public string Name => "get_cooking_remaining_time"; + public string Name => "get_delivery_time"; public async Task Execute(RoleDialogModel message) { diff --git a/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs b/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs index fbd3a6a2..1cc5ac5c 100644 --- a/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs +++ b/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs @@ -12,7 +12,7 @@ public class PizzaBotPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); // Register hooks services.AddScoped();