From fb0adadf6accd2729f468fbb078b965d25399629 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Thu, 29 Feb 2024 23:44:57 -0600 Subject: [PATCH] Unified InvokeFunction. --- README.md | 4 +- docs/conf.py | 4 +- .../Conversations/IConversationService.cs | 2 - .../ConversationService.CallFunctions.cs | 57 ------------------- .../Services/ConversationService.cs | 4 -- .../InstructService.CallFunctions.cs | 51 ----------------- .../Routing/Functions/RouteToAgentFn.cs | 8 ++- .../Handlers/RouteToAgentRoutingHandler.cs | 10 +--- .../Routing/RoutingService.InvokeAgent.cs | 10 +--- .../Routing/RoutingService.InvokeFunction.cs | 46 ++++++++++++++- .../Functions/GetPizzaTypesFn.cs | 34 ++++++++++- .../agent.json | 2 +- .../agent.json | 12 +--- .../agent.json | 2 +- .../agent.json | 4 +- 15 files changed, 95 insertions(+), 155 deletions(-) delete mode 100644 src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Instructs/InstructService.CallFunctions.cs diff --git a/README.md b/README.md index 12d0dc68..fc590da4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ PS D:\> npm run dev Access http://localhost:5015/ -[Online Demo with UI](https://botsharp.azurewebsites.net/) +[Online Demo with UI](https://botsharp.azurewebsites.net/?wt.mc_id=AI-MVP-5005183) @@ -112,7 +112,7 @@ BotSharp uses component design, the kernel is kept to a minimum, and business fu ### Documents -Read the docs: https://botsharp.readthedocs.io +Read the docs: https://botsharp.readthedocs.io?wt.mc_id=AI-MVP-5005183 If you feel that this project is helpful to you, please Star the project, we would be very grateful. diff --git a/docs/conf.py b/docs/conf.py index 2b1636ce..f7eb99ff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,9 +64,9 @@ author = 'Haiping Chen' # built documents. # # The short X.Y version. -version = '0.23' +version = '1.1' # The full version, including alpha/beta/rc tags. -release = '0.23.0' +release = '1.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index 73978b98..72f4ac9e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -35,6 +35,4 @@ public interface IConversationService List GetDialogHistory(int lastCount = 50); Task CleanHistory(string agentId); - - Task CallFunctions(RoleDialogModel msg); } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs deleted file mode 100644 index 6b1ab6ca..00000000 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using BotSharp.Abstraction.Functions; - -namespace BotSharp.Core.Conversations.Services; - -public partial class ConversationService -{ - public async Task CallFunctions(RoleDialogModel msg) - { - // Invoke functions - var functions = _services.GetServices() - .Where(x => x.Name == msg.FunctionName) - .ToList(); - - if (functions.Count == 0) - { - msg.Content = $"Can't find function implementation of {msg.FunctionName}."; - _logger.LogError(msg.Content); - return; - } - - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); - - foreach (var fn in functions) - { - // Before executing functions - foreach (var hook in hooks) - { - await hook.OnFunctionExecuting(msg); - } - - try - { - // Execute function - await fn.Execute(msg); - - if (string.IsNullOrEmpty(msg.Content)) - { - msg.Content = msg.Content ?? JsonSerializer.Serialize(msg.Data); - } - } - catch (Exception ex) - { - msg.Content = ex.Message; - msg.StopCompletion = true; - _logger.LogError(msg.Content); - } - - // After functions have been executed - foreach (var hook in hooks) - { - await hook.OnFunctionExecuted(msg); - } - } - } -} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index d471fd9f..67c75a5c 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -1,7 +1,3 @@ -using BotSharp.Abstraction.Repositories; -using BotSharp.Abstraction.Repositories.Filters; -using BotSharp.Abstraction.Users.Enums; - namespace BotSharp.Core.Conversations.Services; public partial class ConversationService : IConversationService diff --git a/src/Infrastructure/BotSharp.Core/Instructs/InstructService.CallFunctions.cs b/src/Infrastructure/BotSharp.Core/Instructs/InstructService.CallFunctions.cs deleted file mode 100644 index 0e558204..00000000 --- a/src/Infrastructure/BotSharp.Core/Instructs/InstructService.CallFunctions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using BotSharp.Abstraction.Functions; - -namespace BotSharp.Core.Instructs; - -public partial class InstructService -{ - private async Task CallFunctions(RoleDialogModel msg) - { - var hooks = _services.GetServices() - .OrderBy(x => x.Priority).ToList(); - - // Invoke functions - var functions = _services.GetServices() - .Where(x => x.Name == msg.FunctionName) - .ToList(); - - if (functions.Count == 0) - { - msg.Content = $"Can't find function implementation of {msg.FunctionName}."; - _logger.LogError(msg.Content); - return; - } - - foreach (var fn in functions) - { - // Before executing functions - foreach (var hook in hooks) - { - await hook.OnFunctionExecuting(msg); - } - - try - { - // Execute function - await fn.Execute(msg); - } - catch (Exception ex) - { - msg.Content = ex.Message; - msg.StopCompletion = true; - _logger.LogError(msg.Content); - } - - // After functions have been executed - foreach (var hook in hooks) - { - await hook.OnFunctionExecuted(msg); - } - } - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs index 55bcdf82..bca532f0 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -29,6 +29,7 @@ public class RouteToAgentFn : IFunctionCallback { // Correct user goal agent to keep orignal task var goalAgentInState = states.GetState("user_goal_agent", string.Empty); + bool correctToOriginalAgent = false; if (goalAgentInState == string.Empty) { states.SetState("user_goal_agent", args.OriginalAgent, isNeedVersion: true); @@ -37,6 +38,7 @@ public class RouteToAgentFn : IFunctionCallback { // Correct to original agent args.OriginalAgent = goalAgentInState; + correctToOriginalAgent = true; } else if (args.OriginalAgent != args.AgentName && args.OriginalAgent != goalAgentInState) { @@ -49,7 +51,7 @@ public class RouteToAgentFn : IFunctionCallback var originalAgent = db.GetAgents(filter).FirstOrDefault(); if (originalAgent != null) { - _context.Push(originalAgent.Id, $"user goal agent"); + _context.Push(originalAgent.Id, $"user goal agent{(correctToOriginalAgent ? " & is corrected" : "")}"); } } @@ -90,8 +92,8 @@ public class RouteToAgentFn : IFunctionCallback var missingfield = HasMissingRequiredField(message, out var agentId); if (missingfield && message.CurrentAgentId != agentId) { - // Stack original Agent - _context.Push(agentId, reason: "redirection rule"); + // Stack redirection agent + _context.Push(agentId, reason: $"redirection: {message.Content}"); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs index 4aae20db..ae440748 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs @@ -1,7 +1,3 @@ -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; @@ -39,12 +35,10 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler public async Task Handle(IRoutingService routing, FunctionCallFromLlm inst, RoleDialogModel message) { - var context = _services.GetRequiredService(); - var function = _services.GetServices().FirstOrDefault(x => x.Name == inst.Function); message.FunctionArgs = JsonSerializer.Serialize(inst); - var ret = await function.Execute(message); + var ret = await routing.InvokeFunction(message.FunctionName, message); - var agentId = context.GetCurrentAgentId(); + var agentId = routing.Context.GetCurrentAgentId(); // Update next action agent's name var agentService = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index ede6ee42..8518d769 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Routing; @@ -57,15 +55,13 @@ public partial class RoutingService var states = _services.GetRequiredService(); states.SaveStateByArgs(message.FunctionArgs?.JsonContent()); - var conversationService = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); // Call functions - await conversationService.CallFunctions(message); + await routing.InvokeFunction(message.FunctionName, message); // Pass execution result to LLM to get response if (!message.StopCompletion) { - var routing = _services.GetRequiredService(); - // Find response template var templateService = _services.GetRequiredService(); var responseTemplate = await templateService.RenderFunctionResponse(message.CurrentAgentId, message); @@ -83,7 +79,7 @@ public partial class RoutingService content: message.Content)); // Send to Next LLM - var agentId = routing.GetCurrentAgentId(); + var agentId = routing.Context.GetCurrentAgentId(); await InvokeAgent(agentId, dialogs); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index d1be4254..6ef40c58 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -7,16 +7,56 @@ public partial class RoutingService public async Task InvokeFunction(string name, RoleDialogModel message) { var function = _services.GetServices().FirstOrDefault(x => x.Name == name); - if (function == null) return false; + if (function == null) + { + message.StopCompletion = true; + message.Content = $"Can't find function implementation of {message.FunctionName}."; + _logger.LogError(message.Content); + return false; + } var originalFunctionName = message.FunctionName; message.FunctionName = name; message.Role = AgentRole.Function; message.FunctionArgs = message.FunctionArgs; - var result = await function.Execute(message); + + var hooks = _services.GetServices() + .OrderBy(x => x.Priority) + .ToList(); + + // Before executing functions + foreach (var hook in hooks) + { + await hook.OnFunctionExecuting(message); + } + + bool result = false; + + try + { + result = await function.Execute(message); + } + catch (Exception ex) + { + message.StopCompletion = true; + message.Content = ex.Message; + _logger.LogError(ex.ToString()); + } + + // Make sure content has been populated + if (string.IsNullOrEmpty(message.Content) && message.Data != null) + { + message.Content = JsonSerializer.Serialize(message.Data); + } + + // After functions have been executed + foreach (var hook in hooks) + { + await hook.OnFunctionExecuted(message); + } // restore original function name - if (!message.StopCompletion) + if (!message.StopCompletion && message.FunctionName != originalFunctionName) { message.FunctionName = originalFunctionName; } diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs index 436ea4f9..c1694b73 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs @@ -1,4 +1,9 @@ +using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Messaging; +using BotSharp.Abstraction.Messaging.Models.RichContent; +using BotSharp.Abstraction.Messaging.Models.RichContent.Template; +using System.Linq; namespace BotSharp.Plugin.PizzaBot.Functions; @@ -6,15 +11,40 @@ public class GetPizzaTypesFn : IFunctionCallback { public string Name => "get_pizza_types"; + private readonly IServiceProvider _services; + public GetPizzaTypesFn(IServiceProvider services) + { + _services = services; + } + public async Task Execute(RoleDialogModel message) { - message.Content = "Pepperoni Pizza, Cheese Pizza, Margherita Pizza"; - message.Data = new List + var states = _services.GetRequiredService(); + var pizzaTypes = new List { "Pepperoni Pizza", "Cheese Pizza", "Margherita Pizza" }; + message.Data = pizzaTypes; + message.RichContent = new RichContent + { + Recipient = new Recipient + { + Id = states.GetConversationId() + }, + Message = new ButtonTemplateMessage + { + Text = "Please select a pizza type", + Buttons = pizzaTypes.Select(x => new ButtonElement + { + Type = "text", + Title = x, + Payload = x + }).ToArray() + } + }; + 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 3dc3c2fe..4da6bdd3 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 @@ -7,7 +7,7 @@ "createdDateTime": "2023-08-18T10:39:32.2349685Z", "updatedDateTime": "2023-08-18T14:39:32.2349686Z", "iconUrl": "https://cdn-icons-png.flaticon.com/512/6978/6978255.png", - "disabled": true, + "disabled": false, "isPublic": true, "profiles": [ "pizza" ], "routingRules": [ 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 949e201c..4f606251 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 @@ -4,15 +4,7 @@ "createdDateTime": "2023-08-18T14:39:32.2349685Z", "updatedDateTime": "2023-08-18T14:39:32.2349686Z", "id": "b284db86-e9c2-4c25-a59e-4649797dd130", - "disabled": true, + "disabled": false, "isPublic": true, - "profiles": [ "pizza" ], - "routingRules": [ - { - "field": "order_number", - "description": "order number", - "type": "string", - "redirectTo": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd" - } - ] + "profiles": [ "pizza" ] } \ No newline at end of file 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 4751fb78..62f069bb 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 @@ -4,7 +4,7 @@ "createdDateTime": "2023-07-26T02:29:25.123224Z", "updatedDateTime": "2023-07-26T02:29:25.123274Z", "id": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd", - "disabled": true, + "disabled": false, "isPublic": true, "profiles": [ "pizza" ] } \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json index af5d6cd9..1dab7257 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json @@ -4,7 +4,7 @@ "createdDateTime": "2023-07-26T02:29:25.123224Z", "updatedDateTime": "2023-07-26T02:29:25.123274Z", "id": "fe8c60aa-b114-4ef3-93cb-a8efeac80f75", - "disabled": true, + "disabled": false, "isPublic": true, "profiles": [ "pizza" ], "routingRules": [ @@ -13,7 +13,7 @@ "description": "order number", "type": "string", "required": true, - "redirectTo": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd" + "redirectTo": "b284db86-e9c2-4c25-a59e-4649797dd130" } ] } \ No newline at end of file