diff --git a/BotSharp.sln b/BotSharp.sln index 52b3f9d0..5baf8d10 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -121,6 +121,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Core.SideCar", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.VertexAI", "src\Plugins\BotSharp.Plugin.LangChain\BotSharp.Plugin.VertexAI.csproj", "{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Crontab", "src\Infrastructure\BotSharp.Core.Crontab\BotSharp.Core.Crontab.csproj", "{F812BAAE-5A7D-4DF7-8E71-70696B51C61F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -489,6 +491,14 @@ Global {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.Build.0 = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.ActiveCfg = Release|Any CPU {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.Build.0 = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Debug|x64.Build.0 = Debug|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|Any CPU.Build.0 = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x64.ActiveCfg = Release|Any CPU + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -547,6 +557,7 @@ Global {F57F4862-F8D4-44A1-AC12-5C131B5C9785} = {51AFE054-AE99-497D-A593-69BAEFB5106F} {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {7DA2DCD0-551B-432E-AA5C-22DDD3ED459B} = {D5293208-2BEF-42FC-A64C-5954F61720BA} + {F812BAAE-5A7D-4DF7-8E71-70696B51C61F} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/docs/architecture/agent-utility.md b/docs/architecture/agent-utility.md new file mode 100644 index 00000000..b65dea8c --- /dev/null +++ b/docs/architecture/agent-utility.md @@ -0,0 +1,459 @@ +# Agent Utility + +## Introduction +This document aims to introduce the agent utility concept and provide an instruction on adding custom agent utilities in AI project “BotSharp”. We will start by explaining the mechanism of agent utility in [Section 2](#agent-utility). Then, we illustrate the custom agent utility setup and integration in [Section 3](#agent-utility-setup) and [Section 4](#agent-utility-integration), respectively. A use case will be demonstrated in [Section 5](#use-case-demo). We wrap up the document with a short summary. + +## Agent Utility +The agent utility is a unique feature that can be integrated into agent to enhance its capability. Its core principle is that it can dynamically and seamlessly add extra prompts and add task-oriented functions (or tools) during conversation, without disrupting the agent’s primary purpose. In other words, the agent utility can perform extra tasks based on the context of conversation. Typical examples of agent utility include reading images/pdf, generating image, and sending http requests. [Fig 2.1.1](#agent-utility-example) demonstrates an example of these utilities. In this example, “Chatbot” is a simple agent to answer user questions, and the utilities extend its capability to explain image content, generate requested image, and send a specific http request. + +
+ +
+
+ +## Agent Utility Setup +In this section, we outline the steps to set up a custom agent utility. We start with the basic code structure and then add essential utility data, such as prompts and functions. The utility hooks are used to incorporate the utility into the agent. Finally, we give a brief overview of the utility implementation. + +### Basic Code Structure +The basic code structure of a typical agent utility includes prompt/function data, hooks, and function implementation. We can add specific utility prompts and functions in different projects. Note that the agent **“6745151e-6d46-4a02-8de4-1c4f21c7da95”** is considered as a dedicated utility assistant, and every prompt and function can be optionally used as a utility. [Fig 3.1.1](#agent-utility-code-structure) presents the structure of prompt, function, hooks, and implementation of an http utility. + +
+ +
+
+ +### Utility Data +For a typical agent utility, it is essential to add at least one prompt and one function. The utility name is formatted as **“[plugin name].[utility name]”**, such as “http.http-handler”. The prompt is added under the “templates” folder and its recommended name is **“util-[plugin name]-[function name].fn.liquid”**, while the function is added under the “functions” folder and its recommended name is **“util-[plugin name]-[function name].json”**. Once we compile the project, we can find the aggregated utility assistant folder at location: **“\BotSharp\src\WebStarter\bin\Debug\net8.0\data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95”**. + +### Utility Hooks +The utility hooks are used to connect the agent utility to the agent system. The code snippet below demonstrates an implementation of the http utility hook, where we define the utility name, prompts and functions. Note that each utility can be used across different agents. + +```csharp +public class HttpHandlerPlugin : IBotSharpPlugin +{ + public string Id => "2c1eb1c4-16e5-4c65-8ee4-032324c26b81"; + public string Name => "HTTP Handler"; + public string Description => "Empower agent to handle HTTP request in RESTful API or GraphQL"; + public string IconUrl => "https://lirp.cdn-website.com/6f8d6d8a/dms3rep/multi/opt/API_Icon-640w.png"; + public string[] AgentIds => new[] { "87c458fc-ec5f-40ae-8ed6-05dda8a07523" }; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("HttpHandler"); + }); + + services.AddScoped(); + } +} +``` + +The agent hook is used to append the utility prompt and function during the conversation. **Note that the utility data is only allowed to be included in the context of conversation**. The utility mechanism is implemented in the **“OnAgentUtilityLoaded”** hook, and it is invoked when we load any agent. + +```csharp +public async Task LoadAgent(string id) +{ + if (string.IsNullOrEmpty(id) || id == Guid.Empty.ToString()) + { + return null; + } + + var hooks = _services.GetServices(); + + // Before agent is loaded. + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != id) + { + continue; + } + + hook.OnAgentLoading(ref id); + } + + var agent = await GetAgent(id); + if (agent == null) + { + return null; + } + + await InheritAgent(agent); + OverrideInstructionByChannel(agent); + AddOrUpdateParameters(agent); + + // Populate state into dictionary + agent.TemplateDict = new Dictionary(); + PopulateState(agent.TemplateDict); + + // After agent is loaded + foreach (var hook in hooks) + { + if (!string.IsNullOrEmpty(hook.SelfId) && hook.SelfId != id) + { + continue; + } + + hook.SetAget(agent); + + if (!string.IsNullOrEmpty(agent.Instruction)) + { + hook.OnInstructionLoaded(agent.Instruction, agent.TemplateDict); + } + + if (agent.Functions != null) + { + hook.OnFunctionsLoaded(agent.Functions); + } + + if (agent.Samples != null) + { + hook.OnSamplesLoaded(agent.Samples); + } + + hook.OnAgentUtilityLoaded(agent); + hook.OnAgentLoaded(agent); + } + + _logger.LogInformation($"Loaded agent {agent}."); + + return agent; +} +``` + +### Utility Function Implementation +Here we introduce a simple utility function implementation. The actual content depends on what task you want this utility to fulfill. The code snippet below illustrates the implementation of the “handle http request” function. Note that the property “Name” must be consistent with the function added in the utility data. The “Indication” is an optional property whose content will be displayed in the chat while the user is waiting for the assistant response. + +```csharp +public class HandleHttpRequestFn : IFunctionCallback +{ + public string Name => "util-http-handle_http_request"; + public string Indication => "Handling http request"; + + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IHttpContextAccessor _context; + private readonly BotSharpOptions _options; + + public HandleHttpRequestFn(IServiceProvider services, + ILogger logger, + IHttpClientFactory httpClientFactory, + IHttpContextAccessor context, + BotSharpOptions options) + { + _services = services; + _logger = logger; + _httpClientFactory = httpClientFactory; + _context = context; + _options = options; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs, _options.JsonSerializerOptions); + var url = args?.RequestUrl; + var method = args?.HttpMethod; + var content = args?.RequestContent; + + try + { + var response = await SendHttpRequest(url, method, content); + var responseContent = await HandleHttpResponse(response); + message.Content = responseContent; + return true; + } + catch (Exception ex) + { + var msg = $"Fail when sending http request. Url: {url}, method: {method}, content: {content}"; + _logger.LogWarning($"{msg}\n(Error: {ex.Message})"); + message.Content = msg; + return false; + } + } + + private async Task SendHttpRequest(string? url, string? method, string? content) + { + if (string.IsNullOrEmpty(url)) return null; + + using var client = _httpClientFactory.CreateClient(); + AddRequestHeaders(client); + + var (uri, request) = BuildHttpRequest(url, method, content); + var response = await client.SendAsync(request); + if (response == null || !response.IsSuccessStatusCode) + { + _logger.LogWarning($"Response status code: {response?.StatusCode}"); + } + + return response; + } + + private void AddRequestHeaders(HttpClient client) + { + client.DefaultRequestHeaders.Add("Authorization", $"{_context.HttpContext.Request.Headers["Authorization"]}"); + + var settings = _services.GetRequiredService(); + var origin = !string.IsNullOrEmpty(settings.Origin) ? settings.Origin : $"{_context.HttpContext.Request.Headers["Origin"]}"; + if (!string.IsNullOrEmpty(origin)) + { + client.DefaultRequestHeaders.Add("Origin", origin); + } + } + + private (Uri, HttpRequestMessage) BuildHttpRequest(string url, string? method, string? content) + { + var httpMethod = GetHttpMethod(method); + StringContent httpContent; + + var requestUrl = url; + if (httpMethod == HttpMethod.Get) + { + httpContent = BuildHttpContent("{}"); + requestUrl = BuildQuery(url, content); + } + else + { + httpContent = BuildHttpContent(content); + } + + if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out var uri)) + { + var settings = _services.GetRequiredService(); + var baseUri = new Uri(settings.BaseAddress); + uri = new Uri(baseUri, requestUrl); + } + + return (uri, new HttpRequestMessage + { + RequestUri = uri, + Method = httpMethod, + Content = httpContent + }); + } + + private HttpMethod GetHttpMethod(string? method) + { + var localMethod = method?.Trim()?.ToUpper(); + HttpMethod matchMethod; + + switch (localMethod) + { + case "GET": + matchMethod = HttpMethod.Get; + break; + case "DELETE": + matchMethod = HttpMethod.Delete; + break; + case "PUT": + matchMethod = HttpMethod.Put; + break; + case "Patch": + matchMethod = HttpMethod.Patch; + break; + default: + matchMethod = HttpMethod.Post; + break; + } + return matchMethod; + } + + private StringContent BuildHttpContent(string? content) + { + var str = string.Empty; + try + { + var json = JsonSerializer.Deserialize(content ?? "{}", _options.JsonSerializerOptions); + str = JsonSerializer.Serialize(json, _options.JsonSerializerOptions); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when build http content: {content}\n(Error: {ex.Message})"); + } + + return new StringContent(str, Encoding.UTF8, MediaTypeNames.Application.Json); + } + + private string BuildQuery(string url, string? content) + { + if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(content)) return url; + + try + { + var queries = new List(); + var json = JsonSerializer.Deserialize(content, _options.JsonSerializerOptions); + var root = json.RootElement; + foreach (var prop in root.EnumerateObject()) + { + var name = prop.Name.Trim(); + var value = prop.Value.ToString().Trim(); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) + { + continue; + } + + queries.Add($"{name}={value}"); + } + + if (!queries.IsNullOrEmpty()) + { + url += $"?{string.Join('&', queries)}"; + } + return url; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when building url query. Url: {url}, Content: {content}\n(Error: {ex.Message})"); + return url; + } + } + + private async Task HandleHttpResponse(HttpResponseMessage? response) + { + if (response == null) return string.Empty; + + return await response.Content.ReadAsStringAsync(); + } +} +``` + +### Utility Inheritance +Here we introduce a new feature: utility inheritance. As we are using the routing-based multi-agent architecture, different agents may come into the call stack while we are handling user requests. With the utility inheritance, the agent can not only use the utilities added to itself but also inherit the utilities from the entry agent, which is the first agent that comes into the call stack, such as a router agent. [Fig 3.5.1](#routing-architecture) illustrates a typical example of routing-based multi-agent architecture, where “Pizza Bot” is the router and "Order Inquery", "Ordering", "Payment" are task agents. When the user starts chatting, “Pizza Bot” first comes into the call stack, with "Order Inquery", "Ordering", or "Payment" joining next depending on what the user actually requests. For example, if we allow "Order Inquery" to inherit utilities, all the utilities from itself as well as “Pizza Bot” will be invoked once the "Order Inquery" agent is in action. + +
+ +
+
+ +### Agent Setup +Here we introduce the agent setup with utilities and inheritance. As we introduced in [Section 3.3](#utility-hooks), each utility of an agent is structured with utility name, functions, and prompts. [Fig 3.6.1](#router-utility-ui) and [Fig 3.6.2](#task-agent-utility-ui) presents the utility configuration and utility inheritance of “Pizza Bot” and “Order Inquery”, respectively. As is displayed, we can apply the utility configuration and inheritance via agent files or agent detail ui. Note that we can uncheck the box to disable a utility ([Fig 3.6.2](#router-utility-ui)). + +```json +{ + "id": "8970b1e5-d260-4e2c-90b1-f1415a257c18", + "name": "Pizza Bot", + "description": "AI assistant that can help customer place pizza order.", + "type": "routing", + "inheritAgentId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a", + "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": false, + "isPublic": true, + "profiles": [ "pizza" ], + "utilities": [ + { + "name": "http.http-handler", + "functions": [ + { "name": "util-http-handle_http_request" } + ], + "templates": [ + { "name": "util-http-handle_http_request.fn" } + ] + } + ], + "routingRules": [ + { + "type": "reasoner", + "field": "NaiveReasoner" + } + ] +} +``` +
+ + +
+ +
+
+
+
+ +```json +{ + "name": "Order Inquiry", + "description": "Check the 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", + "disabled": false, + "isPublic": true, + "mergeUtility": true, + "profiles": [ "pizza" ] +} +``` +
+ +
+ +
+
+ + +## Agent Utility Integration +In this section, we outline the steps to integrate a custom agent utility, including registering plugin, registering assembly, and adding project reference. + +The code snippet below presents the “Http Handler Plugin” in the “BotSharp.Plugin.HttpHandler”, where we can register the hooks and other essential settings. Note that there is no need to register the function here, since it is automatically registered on the application level. + +```csharp +public class HttpHandlerPlugin : IBotSharpPlugin +{ + public string Id => "2c1eb1c4-16e5-4c65-8ee4-032324c26b81"; + public string Name => "HTTP Handler"; + public string Description => "Empower agent to handle HTTP request in RESTful API or GraphQL"; + public string IconUrl => "https://lirp.cdn-website.com/6f8d6d8a/dms3rep/multi/opt/API_Icon-640w.png"; + public string[] AgentIds => new[] { "87c458fc-ec5f-40ae-8ed6-05dda8a07523" }; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("HttpHandler"); + }); + + services.AddScoped(); + } +} +``` + +[Fig 4.1.1](#register-assembly) demonstrates the utility assembly registration in “appsettings.json”. It is important to note that we are required to add the project reference to the Startup project, e.g., WebStarter. Moreover, we are required to add any new custom agent utility in the “Plugin” folder instead of the “BotSharp” folder. + +
+ +
+
+ + +## Use Case Demo +In this section, we demonstrate an http utility. After we set up and integrate the custom agent utility in backend, we can start the BotSharp-UI and go to any specific agent. [Fig 5.1.1](#add-utility) shows an example of the “Chatbot” agent, where we can add any registered utilities in the highlight section. + +
+ +
+
+ +Once we add the utility, we can initialize a conversation by clicking the bot icon at the top left corner. [Fig 5.1.2](#chat-window-demo) shows the conversation window, where we can find the number of utilities at the left panel. We can also click the agent name to go back to the agent page. + +
+ +
+
+ +Here we use dummy rest APIs (source: https://dummy.restapiexample.com/) for the demo purpose. [Fig 5.1.3](#dummy-http) displays the various http requests sent in the conversation with “Chatbot”. We can see that the “Http Handler” utility has successfully extends the agent to send http request and receive response. + +
+ +
+
+ +## Summary +In this document, we introduce the agent utility concept and provide a step-by-step instruction on adding custom agent utilities in AI project “BotSharp”. + +The agent utility is designed for enhancing the agent capability to perform dedicated tasks, such as sending http request, reading images, and generating images, by adding extra prompts and functions. + +The agent utility setup and integration are explained step by step in [Section 3](#agent-utility-setup) and [Section 4](#agent-utility-integration), respectively. + +We end up the document by demonstrating the Http utility, where we prove the utility can handle various http requests in the chat with agents. diff --git a/docs/architecture/assets/agent-utility/add-utility.png b/docs/architecture/assets/agent-utility/add-utility.png new file mode 100644 index 00000000..cda3472d Binary files /dev/null and b/docs/architecture/assets/agent-utility/add-utility.png differ diff --git a/docs/architecture/assets/agent-utility/agent-utility-code-structure.png b/docs/architecture/assets/agent-utility/agent-utility-code-structure.png new file mode 100644 index 00000000..8b2c4458 Binary files /dev/null and b/docs/architecture/assets/agent-utility/agent-utility-code-structure.png differ diff --git a/docs/architecture/assets/agent-utility/agent-utility-example.png b/docs/architecture/assets/agent-utility/agent-utility-example.png new file mode 100644 index 00000000..81d1a7d4 Binary files /dev/null and b/docs/architecture/assets/agent-utility/agent-utility-example.png differ diff --git a/docs/architecture/assets/agent-utility/chat-window-demo.png b/docs/architecture/assets/agent-utility/chat-window-demo.png new file mode 100644 index 00000000..e31c6953 Binary files /dev/null and b/docs/architecture/assets/agent-utility/chat-window-demo.png differ diff --git a/docs/architecture/assets/agent-utility/dummy-http.png b/docs/architecture/assets/agent-utility/dummy-http.png new file mode 100644 index 00000000..d5ca8df7 Binary files /dev/null and b/docs/architecture/assets/agent-utility/dummy-http.png differ diff --git a/docs/architecture/assets/agent-utility/register-assembly.png b/docs/architecture/assets/agent-utility/register-assembly.png new file mode 100644 index 00000000..7c5a478e Binary files /dev/null and b/docs/architecture/assets/agent-utility/register-assembly.png differ diff --git a/docs/architecture/assets/agent-utility/router-utility-ui.png b/docs/architecture/assets/agent-utility/router-utility-ui.png new file mode 100644 index 00000000..5e3f887c Binary files /dev/null and b/docs/architecture/assets/agent-utility/router-utility-ui.png differ diff --git a/docs/architecture/assets/agent-utility/routing-arch.png b/docs/architecture/assets/agent-utility/routing-arch.png new file mode 100644 index 00000000..6485c3bb Binary files /dev/null and b/docs/architecture/assets/agent-utility/routing-arch.png differ diff --git a/docs/architecture/assets/agent-utility/task-agent-utility-ui.png b/docs/architecture/assets/agent-utility/task-agent-utility-ui.png new file mode 100644 index 00000000..9838673f Binary files /dev/null and b/docs/architecture/assets/agent-utility/task-agent-utility-ui.png differ diff --git a/docs/index.rst b/docs/index.rst index b7c8b78e..d9dac903 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -115,6 +115,7 @@ The main documentation for the site is organized into the following sections: architecture/plugin architecture/hooks architecture/routing + architecture/agent-utility architecture/logging architecture/data-persistence diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs index 808f7821..a3746ca8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs @@ -49,7 +49,11 @@ public abstract class AgentHookBase : IAgentHook return true; } - public virtual void OnAgentLoaded(Agent agent) + public virtual void OnAgentLoaded(Agent agent) + { + } + + public virtual void OnAgentUtilityLoaded(Agent agent) { } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs index 1bb221d3..34e8ac19 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs @@ -17,7 +17,8 @@ public enum AgentField Response, Sample, LlmConfig, - Utility + Utility, + MaxMessageCount } public enum AgentTaskField diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs index 53d64a07..455e87ff 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentType.cs @@ -10,7 +10,7 @@ public class AgentType /// /// Planning agent /// - public const string Planning = "plan"; + public const string Planning = "planning"; public const string Evaluating = "evaluation"; diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs index c3c69f08..ecdda1c7 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/BuiltInAgentId.cs @@ -32,11 +32,6 @@ public class BuiltInAgentId /// public const string Learner = "01acc3e5-0af7-49e6-ad7a-a760bd12dc40"; - /// - /// Plan feasible implementation steps for complex problems - /// - public const string Planner = "282a7128-69a1-44b0-878c-a9159b88f3b9"; - /// /// SQL statement generation /// @@ -51,4 +46,9 @@ public class BuiltInAgentId /// Evaluate prompt and conversation /// public const string Evaluator = "dfd9b46d-d00c-40af-8a75-3fbdc2b89869"; + + /// + /// Translates user-defined natural language rules into programmatic code + /// + public const string RuleEncoder = "6acfb93c-3412-402e-9ba5-c5d3cd8f0161"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs index 99cdadca..a3f53fb6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs @@ -25,6 +25,8 @@ public interface IAgentHook bool OnSamplesLoaded(List samples); + void OnAgentUtilityLoaded(Agent agent); + /// /// Triggered when agent is loaded completely. /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index 1461cc22..b8945f40 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -59,5 +59,5 @@ public interface IAgentService PluginDef GetPlugin(string agentId); - IEnumerable GetAgentUtilities(); + IEnumerable GetAgentUtilityOptions(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentUtilityHook.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentUtilityHook.cs index 9d5667b8..603ec64e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentUtilityHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentUtilityHook.cs @@ -2,5 +2,5 @@ namespace BotSharp.Abstraction.Agents; public interface IAgentUtilityHook { - void AddUtilities(List utilities); + void AddUtilities(List utilities); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs index 43f0f5ba..41b3800c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs @@ -87,19 +87,29 @@ public class Agent /// /// Profile by channel /// - public List Profiles { get; set; } - = new List(); + public List Profiles { get; set; } = new(); + + /// + /// Merge utilities from entry agent + /// + public bool MergeUtility { get; set; } /// /// Agent utilities /// - public List Utilities { get; set; } = new(); + public List Utilities { get; set; } = new(); /// /// Inherit from agent /// public string? InheritAgentId { get; set; } + /// + /// Maximum message count when load conversation + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxMessageCount { get; set; } + public List RoutingRules { get; set; } = new(); /// @@ -129,6 +139,8 @@ public class Agent Knowledges = agent.Knowledges, IsPublic = agent.IsPublic, Disabled = agent.Disabled, + MergeUtility = agent.MergeUtility, + MaxMessageCount = agent.MaxMessageCount, Profiles = agent.Profiles, RoutingRules = agent.RoutingRules, LlmConfig = agent.LlmConfig, @@ -173,9 +185,9 @@ public class Agent return this; } - public Agent SetUtilities(List utilities) + public Agent SetUtilities(List utilities) { - Utilities = utilities ?? new List(); + Utilities = utilities ?? new List(); return this; } @@ -215,6 +227,12 @@ public class Agent return this; } + public Agent SetMergeUtility(bool merge) + { + MergeUtility = merge; + return this; + } + public Agent SetAgentType(string type) { Type = type; diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentUtility.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentUtility.cs new file mode 100644 index 00000000..ce39c568 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentUtility.cs @@ -0,0 +1,61 @@ +namespace BotSharp.Abstraction.Agents.Models; + +public class AgentUtility +{ + public string Name { get; set; } + public bool Disabled { get; set; } + public IEnumerable Functions { get; set; } = []; + public IEnumerable Templates { get; set; } = []; + + public AgentUtility() + { + + } + + public AgentUtility( + string name, + IEnumerable? functions = null, + IEnumerable? templates = null) + { + Name = name; + Functions = functions ?? []; + Templates = templates ?? []; + } + + public override string ToString() + { + return Name; + } +} + + +public class UtilityFunction : UtilityBase +{ + public UtilityFunction() + { + + } + + public UtilityFunction(string name) + { + Name = name; + } +} + +public class UtilityTemplate : UtilityBase +{ + public UtilityTemplate() + { + + } + + public UtilityTemplate(string name) + { + Name = name; + } +} + +public class UtilityBase +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index 5be126b8..9b31ae6f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs index 8f1cee10..abdfd67b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Enums/BroswerActionEnum.cs @@ -7,4 +7,5 @@ public enum BroswerActionEnum Typing = 3, Hover = 4, Scroll = 5, + DragAndDrop = 6 } diff --git a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs index fd95bd66..9a1ef0d3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/ElementPosition.cs @@ -5,4 +5,9 @@ public class ElementPosition public float X { get; set; } = default!; public float Y { get; set; } = default!; + + public override string ToString() + { + return $"[{X}, {Y}]"; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs index 9239b865..da87646e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs @@ -2,27 +2,23 @@ namespace BotSharp.Abstraction.Conversations; public abstract class ConversationHookBase : IConversationHook { - protected Agent _agent; - public Agent Agent => _agent; + public Agent Agent { get; private set; } - protected Conversation _conversation; - public Conversation Conversation => _conversation; + public Conversation Conversation { get; private set; } - protected List _dialogs; - public List Dialogs => _dialogs; + public List Dialogs { get; private set; } - protected int _priority = 0; - public int Priority => _priority; + public int Priority { get; protected set; } = 0; public IConversationHook SetAgent(Agent agent) { - _agent = agent; + Agent = agent; return this; } public IConversationHook SetConversation(Conversation conversation) { - _conversation = conversation; + Conversation = conversation; return this; } @@ -37,7 +33,7 @@ public abstract class ConversationHookBase : IConversationHook public virtual Task OnDialogsLoaded(List dialogs) { - _dialogs = dialogs; + Dialogs = dialogs; return Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookProvider.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookProvider.cs new file mode 100644 index 00000000..00393da4 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookProvider.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Abstraction.Conversations; + +public class ConversationHookProvider +{ + public IEnumerable Hooks { get; } + + private readonly Lazy> _hooksOrderByPriority; + + public IEnumerable HooksOrderByPriority + => _hooksOrderByPriority.Value; + + public ConversationHookProvider(IEnumerable conversationHooks) + { + Hooks = conversationHooks; + _hooksOrderByPriority = new Lazy>(() => + { + return conversationHooks.OrderBy(hook => hook.Priority).ToArray(); + }); + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ConversationChannel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ConversationChannel.cs index cf0defbc..d03a4208 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ConversationChannel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Enums/ConversationChannel.cs @@ -7,4 +7,5 @@ public class ConversationChannel public const string Phone = "phone"; public const string Messenger = "messenger"; public const string Email = "email"; + public const string Cron = "cron"; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index ffb4986a..6a04e796 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -12,6 +12,7 @@ public interface IConversationService Task GetConversation(string id); Task> GetConversations(ConversationFilter filter); Task UpdateConversationTitle(string id, string title); + Task UpdateConversationTitleAlias(string id, string titleAlias); Task UpdateConversationTags(string conversationId, List tags); Task UpdateConversationMessage(string conversationId, UpdateMessageRequest request); Task> GetLastConversations(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs index 5ded2c7c..a485a9c4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/Conversation.cs @@ -13,6 +13,7 @@ public class Conversation /// public string? TaskId { get; set; } public string Title { get; set; } = string.Empty; + public string TitleAlias { get; set; } = string.Empty; [JsonIgnore] public List Dialogs { get; set; } = new(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index 679527a3..5a67216d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -2,9 +2,11 @@ using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; +using System.Diagnostics; namespace BotSharp.Abstraction.Conversations.Models; +[DebuggerStepThrough] public class RoleDialogModel : ITrackableMessage { /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs new file mode 100644 index 00000000..6a9dd43a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItem.cs @@ -0,0 +1,33 @@ +namespace BotSharp.Abstraction.Crontab.Models; + +public class CrontabItem : ScheduleTaskArgs +{ + [JsonPropertyName("user_id")] + public string UserId { get; set; } = null!; + + [JsonPropertyName("agent_id")] + public string AgentId { get; set; } = null!; + + [JsonPropertyName("conversation_id")] + public string ConversationId { get; set; } = null!; + + [JsonPropertyName("execution_result")] + public string ExecutionResult { get; set; } = null!; + + [JsonPropertyName("execution_count")] + public int ExecutionCount { get; set; } + + [JsonPropertyName("max_execution_count")] + public int MaxExecutionCount { get; set; } + + [JsonPropertyName("expire_seconds")] + public int ExpireSeconds { get; set; } = 60; + + [JsonPropertyName("created_time")] + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + public override string ToString() + { + return $"{Title}: {Description} [AgentId: {AgentId}, UserId: {UserId}]"; + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItemFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItemFilter.cs new file mode 100644 index 00000000..7a949dc2 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/CrontabItemFilter.cs @@ -0,0 +1,23 @@ +namespace BotSharp.Abstraction.Crontab.Models; + +public class CrontabItemFilter : Pagination +{ + [JsonPropertyName("user_ids")] + public IEnumerable? UserIds { get; set; } + + [JsonPropertyName("agent_ids")] + public IEnumerable? AgentIds { get; set; } + + [JsonPropertyName("conversation_ids")] + public IEnumerable? ConversationIds { get; set; } + + public CrontabItemFilter() + { + + } + + public static CrontabItemFilter Empty() + { + return new CrontabItemFilter(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/ScheduleTaskArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/ScheduleTaskArgs.cs new file mode 100644 index 00000000..af28374a --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Crontab/Models/ScheduleTaskArgs.cs @@ -0,0 +1,31 @@ +namespace BotSharp.Abstraction.Crontab.Models; + +public class ScheduleTaskArgs +{ + [JsonPropertyName("cron_expression")] + public string Cron { get; set; } = null!; + + [JsonPropertyName("less_than_60_seconds")] + public bool LessThan60Seconds { get; set; } = false; + + [JsonPropertyName("title")] + public string Title { get; set; } = null!; + + [JsonPropertyName("description")] + public string Description { get; set; } = null!; + + [JsonPropertyName("to_do_list")] + public ScheduleTaskItemArgs[] Tasks { get; set; } = []; +} + +public class ScheduleTaskItemArgs +{ + [JsonPropertyName("topic")] + public string Topic { get; set; } = null!; + + [JsonPropertyName("script")] + public string Script { get; set; } = null!; + + [JsonPropertyName("language")] + public string Language { get; set; } = null!; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/EventPriority.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/EventPriority.cs new file mode 100644 index 00000000..75c4402d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Enums/EventPriority.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Infrastructures.Enums; + +public enum EventPriority +{ + Low = 1, + Medium = 2, + High = 3 +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventPublisher.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventPublisher.cs index e242937e..58b18cba 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventPublisher.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventPublisher.cs @@ -10,7 +10,11 @@ public interface IEventPublisher /// Task BroadcastAsync(string channel, string message); - Task PublishAsync(string channel, string message); + Task PublishAsync(string channel, string message, EventPriority? priority = null); Task ReDispatchAsync(string channel, int count = 10, string order = "asc"); + + Task ReDispatchPendingAsync(string channel, string group, int count = 10); + + Task RemoveAsync(string channel, int count = 10); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventSubscriber.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventSubscriber.cs index f295e54e..465644ad 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventSubscriber.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/Events/IEventSubscriber.cs @@ -1,8 +1,12 @@ +using System.Threading; + namespace BotSharp.Abstraction.Infrastructures.Events; public interface IEventSubscriber { Task SubscribeAsync(string channel, Func received); - Task SubscribeAsync(string channel, string group, Func received); + Task SubscribeAsync(string channel, string group, int? port, bool priorityEnabled, + Func received, + CancellationToken? stoppingToken = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/IDistributedLocker.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/IDistributedLocker.cs new file mode 100644 index 00000000..2f5a8069 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/IDistributedLocker.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Infrastructures; + +public interface IDistributedLocker +{ + bool Lock(string resource, Action action, int timeout = 30); + Task LockAsync(string resource, Func action, int timeout = 30); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheAttribute.cs index c8b29ecd..c8f6a674 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheAttribute.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Infrastructures/SharpCacheAttribute.cs @@ -50,7 +50,9 @@ public class SharpCacheAttribute : MoAttribute } var httpContext = Services.GetRequiredService(); - if (httpContext.HttpContext.Response.Headers["Cache-Control"].ToString().Contains("no-store")) + if (httpContext != null && + httpContext.HttpContext != null && + httpContext.HttpContext.Response.Headers["Cache-Control"].ToString().Contains("no-store")) { return; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs b/src/Infrastructure/BotSharp.Abstraction/Planning/IExecutor.cs similarity index 76% rename from src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs rename to src/Infrastructure/BotSharp.Abstraction/Planning/IExecutor.cs index c8bffe6c..3362f1a4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IExecutor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Planning/IExecutor.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.Routing; -namespace BotSharp.Abstraction.Routing.Planning; +namespace BotSharp.Abstraction.Planning; public interface IExecutor { diff --git a/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs b/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs index 238dbffc..4dcff7cb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Planning/IPlanningHook.cs @@ -5,6 +5,9 @@ public interface IPlanningHook Task GetSummaryAdditionalRequirements(string planner, RoleDialogModel message) => Task.FromResult(string.Empty); + Task OnSourceCodeGenerated(string planner, RoleDialogModel msg, string language) + => Task.CompletedTask; + Task OnPlanningCompleted(string planner, RoleDialogModel msg) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Planning/ITaskPlanner.cs b/src/Infrastructure/BotSharp.Abstraction/Planning/ITaskPlanner.cs index a5668a52..2bd8da83 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Planning/ITaskPlanner.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Planning/ITaskPlanner.cs @@ -1,9 +1,20 @@ +using BotSharp.Abstraction.Functions.Models; + namespace BotSharp.Abstraction.Planning; /// /// Planning process for Task Agent +/// https://www.promptingguide.ai/techniques/cot /// -public class ITaskPlanner +public interface ITaskPlanner { - + string Name => "Unamed Task Planner"; + Task GetNextInstruction(Agent router, string messageId, List dialogs); + Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs); + Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs); + List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + => dialogs; + bool AfterHandleContext(List dialogs, List taskAgentDialogs) + => true; + int MaxLoopCount => 5; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginDef.cs b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginDef.cs index 2624bc17..0e2be5f9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginDef.cs @@ -6,14 +6,15 @@ public class PluginDef public string Name { get; set; } public string Description { get; set; } public string Assembly { get; set; } + [JsonPropertyName("is_core")] - public bool IsCore => Assembly == "BotSharp.Core" || Assembly == "BotSharp.Core.SideCar"; + public bool IsCore => Assembly?.StartsWith("BotSharp.Core") == true; [JsonPropertyName("icon_url")] public string? IconUrl { get; set; } [JsonPropertyName("agent_ids")] - public string[] AgentIds { get; set; } = new string[0]; + public string[] AgentIds { get; set; } = []; [JsonPropertyName("settings_name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs index b3c26033..19f5b141 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/BotSharpDatabaseSettings.cs @@ -2,18 +2,19 @@ namespace BotSharp.Abstraction.Repositories; public class BotSharpDatabaseSettings : DatabaseBasicSettings { - public string[] Assemblies { get; set; } - public string FileRepository { get; set; } - public string BotSharpMongoDb { get; set; } - public string TablePrefix { get; set; } - public DbConnectionSetting BotSharp { get; set; } - public string Redis { get; set; } + public string[] Assemblies { get; set; } = []; + public string FileRepository { get; set; } = string.Empty; + public string BotSharpMongoDb { get; set; } = string.Empty; + public string TablePrefix { get; set; } = string.Empty; + public DbConnectionSetting BotSharp { get; set; } = new(); + public string Redis { get; set; } = string.Empty; + public bool EnableReplica { get; set; } = true; } public class DatabaseBasicSettings { - public string Default { get; set; } - public DbConnectionSetting DefaultConnection { get; set; } + public string Default { get; set; } = string.Empty; + public DbConnectionSetting DefaultConnection { get; set; } = new(); public bool EnableSqlLog { get; set; } public bool EnableSensitiveDataLogging { get; set; } public bool EnableRetryOnFailure { get; set; } @@ -23,9 +24,11 @@ public class DbConnectionSetting { public string Master { get; set; } public string[] Slavers { get; set; } + public int ConnectionTimeout { get; set; } = 30; + public int ExecutionTimeout { get; set; } = 30; public DbConnectionSetting() { - Slavers = new string[0]; + Slavers = []; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs index ab1a0a9f..23543049 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/ConversationFilter.cs @@ -8,6 +8,7 @@ public class ConversationFilter /// public string? Id { get; set; } public string? Title { get; set; } + public string? TitleAlias { get; set; } public string? AgentId { get; set; } public string? Status { get; set; } public string? Channel { get; set; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/RoleFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/RoleFilter.cs index 35e2e1d3..7c6c738a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/RoleFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/Filters/RoleFilter.cs @@ -15,4 +15,9 @@ public class RoleFilter { return new RoleFilter(); } + + public bool IsInit() + { + return Names.IsNullOrEmpty(); + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index f3859a3a..4391f970 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -5,6 +5,7 @@ using BotSharp.Abstraction.Roles.Models; using BotSharp.Abstraction.Shared; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; using BotSharp.Abstraction.VectorStorage.Models; @@ -26,15 +27,19 @@ public interface IBotSharpRepository : IHaveServiceProvider #region User User? GetUserByEmail(string email) => throw new NotImplementedException(); - User? GetUserByPhone(string phone, string role = null, string regionCode = "CN") => throw new NotImplementedException(); + User? GetUserByPhone(string phone, string type = UserType.Client, string regionCode = "CN") => throw new NotImplementedException(); User? GetAffiliateUserByPhone(string phone) => throw new NotImplementedException(); User? GetUserById(string id) => throw new NotImplementedException(); List GetUserByIds(List ids) => throw new NotImplementedException(); List GetUsersByAffiliateId(string affiliateId) => throw new NotImplementedException(); User? GetUserByUserName(string userName) => throw new NotImplementedException(); + Dashboard? GetDashboard(string id = null) => throw new NotImplementedException(); void CreateUser(User user) => throw new NotImplementedException(); void UpdateExistUser(string userId, User user) => throw new NotImplementedException(); void UpdateUserVerified(string userId) => throw new NotImplementedException(); + void AddDashboardConversation(string userId, string conversationId) => throw new NotImplementedException(); + void RemoveDashboardConversation(string userId, string conversationId) => throw new NotImplementedException(); + void UpdateDashboardConversation(string userId, DashboardConversation dashConv) => throw new NotImplementedException(); void UpdateUserVerificationCode(string userId, string verficationCode) => throw new NotImplementedException(); void UpdateUserPassword(string userId, string password) => throw new NotImplementedException(); void UpdateUserEmail(string userId, string email) => throw new NotImplementedException(); @@ -48,7 +53,7 @@ public interface IBotSharpRepository : IHaveServiceProvider #region Agent void UpdateAgent(Agent agent, AgentField field); - Agent? GetAgent(string agentId); + Agent? GetAgent(string agentId, bool basicsOnly = false); List GetAgents(AgentFilter filter); List GetUserAgents(string userId); void BulkInsertAgents(List agents); @@ -81,6 +86,7 @@ public interface IBotSharpRepository : IHaveServiceProvider Conversation GetConversation(string conversationId); PagedItems GetConversations(ConversationFilter filter); void UpdateConversationTitle(string conversationId, string title); + void UpdateConversationTitleAlias(string conversationId, string titleAlias); bool UpdateConversationTags(string conversationId, List tags); bool UpdateConversationMessage(string conversationId, UpdateMessageRequest request); void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); @@ -140,4 +146,10 @@ public interface IBotSharpRepository : IHaveServiceProvider bool DeleteKnolwedgeBaseFileMeta(string collectionName, string vectorStoreProvider, Guid? fileId = null); PagedItems GetKnowledgeBaseFileMeta(string collectionName, string vectorStoreProvider, KnowledgeFileFilter filter); #endregion + + #region Crontab + bool UpsertCrontabItem(CrontabItem cron) => throw new NotImplementedException(); + bool DeleteCrontabItem(string conversationId) => throw new NotImplementedException(); + PagedItems GetCrontabItems(CrontabItemFilter filter) => throw new NotImplementedException(); + #endregion } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs index 1d1913dd..c595d59c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Enums/RuleType.cs @@ -12,6 +12,11 @@ public class RuleType /// public const string DataValidation = "data-validation"; + /// + /// The reasoning approach name for next step + /// + public const string Reasoner = "reasoner"; + /// /// The planning approach name for next step /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs index f75e8e0e..1dd9f50a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs @@ -6,6 +6,7 @@ public interface IRoutingContext string FirstGoalAgentId(); bool ContainsAgentId(string agentId); string OriginAgentId { get; } + string EntryAgentId { get; } string ConversationId { get; } string MessageId { get; } void SetMessageId(string conversationId, string messageId); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutableAgent.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutableAgent.cs index 14fd6826..861f3f2f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutableAgent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutableAgent.cs @@ -13,6 +13,9 @@ public class RoutableAgent [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; + [JsonPropertyName("type")] + public string Type { get; set; } = AgentType.Task; + [JsonPropertyName("profiles")] public List Profiles { get; set; } = new List(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IRoutingPlaner.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IRoutingPlaner.cs deleted file mode 100644 index 7f3abde9..00000000 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Planning/IRoutingPlaner.cs +++ /dev/null @@ -1,19 +0,0 @@ -using BotSharp.Abstraction.Functions.Models; - -namespace BotSharp.Abstraction.Routing.Planning; - -/// -/// Task breakdown and execution plan -/// https://www.promptingguide.ai/techniques/cot -/// -public interface IRoutingPlaner -{ - Task GetNextInstruction(Agent router, string messageId, List dialogs); - Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs); - Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs); - List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) - => dialogs; - bool AfterHandleContext(List dialogs, List taskAgentDialogs) - => true; - int MaxLoopCount => 5; -} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Reasoning/IRoutingReasoner.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Reasoning/IRoutingReasoner.cs new file mode 100644 index 00000000..f7bb5eb4 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Reasoning/IRoutingReasoner.cs @@ -0,0 +1,30 @@ +using BotSharp.Abstraction.Functions.Models; + +namespace BotSharp.Abstraction.Routing.Reasoning; + +/// +/// Reasoning approaches for large language models (LLMs) help enhance their ability to solve complex problems, +/// handle tasks requiring logic, and provide accurate and contextually appropriate responses. +/// +public interface IRoutingReasoner +{ + string Name => "Unnamed Reasoner"; + string Description => "Each of these approaches leverages the capabilities of LLMs to reason more effectively, " + + "ensuring better performance and more coherent outputs across various types of complex tasks."; + + int MaxLoopCount => 5; + + Task GetNextInstruction(Agent router, string messageId, List dialogs); + + Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + => Task.FromResult(true); + + Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + => Task.FromResult(true); + + List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + => dialogs; + + bool AfterHandleContext(List dialogs, List taskAgentDialogs) + => true; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs index 9a1316fb..84b65573 100644 --- a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs @@ -9,5 +9,6 @@ public interface IConversationSideCar List GetConversationDialogs(string conversationId); void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); ConversationBreakpoint? GetConversationBreakpoint(string conversationId); - Task SendMessage(string agentId, string text, PostbackMessageModel? postback = null, List? states = null); + Task SendMessage(string agentId, string text, + PostbackMessageModel? postback = null, List? states = null, List? dialogs = null); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs b/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs index e5ddeb61..82eff1c1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Templating/ITemplateRender.cs @@ -3,4 +3,5 @@ namespace BotSharp.Abstraction.Templating; public interface ITemplateRender { string Render(string template, Dictionary dict); + void Register(Type type); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs index b565a260..b702ea01 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs @@ -1,5 +1,8 @@ namespace BotSharp.Abstraction.Users.Enums; +/// +/// User actions on agent level +/// public static class UserAction { public const string Edit = "edit"; diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs index 58dfc186..64212fe4 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs @@ -1,5 +1,8 @@ namespace BotSharp.Abstraction.Users.Enums; +/// +/// User permission +/// public static class UserPermission { public const string CreateAgent = "create-agent"; diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index cc67c1f7..134ca25b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -29,4 +29,8 @@ public interface IUserService Task UpdatePassword(string newPassword, string verificationCode); Task GetUserTokenExpires(); Task UpdateUsersIsDisable(List userIds, bool isDisable); + Task AddDashboardConversation(string userId, string conversationId); + Task RemoveDashboardConversation(string userId, string conversationId); + Task UpdateDashboardConversation(string userId, DashboardConversation dashConv); + Task GetDashboard(string userId); } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/Dashboard.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/Dashboard.cs new file mode 100644 index 00000000..753cb390 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/Dashboard.cs @@ -0,0 +1,20 @@ + +namespace BotSharp.Abstraction.Users.Models; + +public class Dashboard +{ + public IList ConversationList { get; set; } = []; +} + +public class DashboardComponent +{ + public required string Id { get; set; } + public string? Name { get; set; } +} + +public class DashboardConversation : DashboardComponent +{ + public string? ConversationId { get; set; } + public string? Instruction { get; set; } = ""; +} + diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs index 8d136dc5..7699a58b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs @@ -23,6 +23,7 @@ public class User public bool Verified { get; set; } public string RegionCode { get; set; } = "CN"; public string? AffiliateId { get; set; } + public string? ReferralCode { get; set; } public string? EmployeeId { get; set; } public bool IsDisabled { get; set; } public IEnumerable Permissions { get; set; } = []; diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs index 56df6a7d..f3c4c7f0 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAuthorization.cs @@ -22,4 +22,26 @@ public static class UserAuthorizationExtension var actions = found.Actions ?? []; return actions.Any(x => x == targetAction); } + + /// + /// Get allowed user actions on the agent. If user is admin, returns null; + /// + /// + /// + /// + public static IEnumerable? GetAllowedAgentActions(this UserAuthorization auth, string agentId) + { + if (auth == null || string.IsNullOrEmpty(agentId)) + { + return []; + } + + if (auth.IsAdmin) + { + return null; + } + + var found = auth.AgentActions.FirstOrDefault(x => x.AgentId == agentId); + return found?.Actions ?? []; + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Using.cs b/src/Infrastructure/BotSharp.Abstraction/Using.cs index 825f7d8f..592803c2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Using.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Using.cs @@ -12,10 +12,10 @@ global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Infrastructures.Enums; global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Routing.Models; -global using BotSharp.Abstraction.Routing.Planning; global using BotSharp.Abstraction.Templating; global using BotSharp.Abstraction.Translation.Attributes; global using BotSharp.Abstraction.Messaging.Enums; global using BotSharp.Abstraction.Files.Models; global using BotSharp.Abstraction.Files.Enums; -global using BotSharp.Abstraction.Knowledges.Models; \ No newline at end of file +global using BotSharp.Abstraction.Knowledges.Models; +global using BotSharp.Abstraction.Crontab.Models; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs new file mode 100644 index 00000000..4ea18179 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/MathExt.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Abstraction.Utilities; + +public static class MathExt +{ + public static int Max(int a, int b, int c) + { + return Math.Max(Math.Max(a, b), c); + } + + public static long Max(long a, long b, long c) + { + return Math.Max(Math.Max(a, b), c); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs index 8e5c7f23..4d21013b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/Pagination.cs @@ -1,7 +1,9 @@ using BotSharp.Abstraction.Infrastructures; +using System.Diagnostics; namespace BotSharp.Abstraction.Utilities; +[DebuggerStepThrough] public class Pagination : ICacheKey { private int _page; diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Abstraction/ICrontabHook.cs b/src/Infrastructure/BotSharp.Core.Crontab/Abstraction/ICrontabHook.cs new file mode 100644 index 00000000..1738ef5e --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Abstraction/ICrontabHook.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Core.Crontab.Abstraction; + +public interface ICrontabHook +{ + Task OnCronTriggered(CrontabItem item); +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Abstraction/ICrontabService.cs b/src/Infrastructure/BotSharp.Core.Crontab/Abstraction/ICrontabService.cs new file mode 100644 index 00000000..39c13779 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Abstraction/ICrontabService.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Core.Crontab.Abstraction; + +public interface ICrontabService +{ + Task> GetCrontable(); + Task ScheduledTimeArrived(CrontabItem item); +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj b/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj new file mode 100644 index 00000000..cc072fe6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/BotSharp.Core.Crontab.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs b/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs new file mode 100644 index 00000000..26f5cfad --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/CrontabPlugin.cs @@ -0,0 +1,38 @@ +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + +using BotSharp.Core.Crontab.Hooks; + +namespace BotSharp.Core.Crontab; + +/// +/// Crontab plugin is a time-based job scheduler in agent framework. +/// The cron system is used for automating repetitive tasks, such as trigger AI Agent to do specific task periodically. +/// +public class CrontabPlugin : IBotSharpPlugin +{ + public string Id => "3155c15e-28d3-43f7-8ead-fc43324ec21a"; + public string Name => "BotSharp Crontab"; + public string Description => "Crontab plugin is a time-based job scheduler in agent framework. The cron system is used to trigger AI Agent to do specific task periodically."; + public string IconUrl => "https://icon-library.com/images/stop-watch-icon/stop-watch-icon-10.jpg"; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + services.AddScoped(); + services.AddScoped(); + services.AddHostedService(); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Enum/UtilityName.cs b/src/Infrastructure/BotSharp.Core.Crontab/Enum/UtilityName.cs new file mode 100644 index 00000000..eba15bd9 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Enum/UtilityName.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Core.Crontab.Enum; + +public class UtilityName +{ + public const string ScheduleTask = "crontab.schedule-task"; +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Functions/ScheduleTaskFn.cs b/src/Infrastructure/BotSharp.Core.Crontab/Functions/ScheduleTaskFn.cs new file mode 100644 index 00000000..b538cb3d --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Functions/ScheduleTaskFn.cs @@ -0,0 +1,55 @@ +using BotSharp.Abstraction.Repositories; +using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Users; +using BotSharp.Core.Crontab.Hooks; + +namespace BotSharp.Core.Crontab.Functions; + +public class ScheduleTaskFn : IFunctionCallback +{ + public string Name => $"{CrontabUtilityHook.PREFIX}schedule_task"; + private readonly IServiceProvider _services; + + public ScheduleTaskFn(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + if (args.LessThan60Seconds) + { + message.Content = "Cron expression should not include seconds."; + return false; + } + + var routing = _services.GetRequiredService(); + var user = _services.GetRequiredService(); + var db = _services.GetRequiredService(); + + if (string.IsNullOrEmpty(args.Cron)) + { + var ret = db.DeleteCrontabItem(routing.ConversationId); + message.Content = $"Task schedule canceled result: {ret}"; + } + else + { + var crontabItem = new CrontabItem + { + Title = args.Title, + Description = args.Description, + Cron = args.Cron, + UserId = user.Id, + AgentId = routing.EntryAgentId, + ConversationId = routing.ConversationId, + Tasks = args.Tasks, + }; + + var ret = db.UpsertCrontabItem(crontabItem); + message.Content = $"Task scheduled result: {ret}"; + } + + return true; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Hooks/CrontabUtilityHook.cs b/src/Infrastructure/BotSharp.Core.Crontab/Hooks/CrontabUtilityHook.cs new file mode 100644 index 00000000..ed150861 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Hooks/CrontabUtilityHook.cs @@ -0,0 +1,25 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Core.Crontab.Enum; + +namespace BotSharp.Core.Crontab.Hooks; + +public class CrontabUtilityHook : IAgentUtilityHook +{ + public const string PREFIX = "util-crontab-"; + private const string SCHEDULE_TASK_FN = $"{PREFIX}schedule_task"; + + public void AddUtilities(List utilities) + { + var items = new List + { + new AgentUtility + { + Name = UtilityName.ScheduleTask, + Functions = [new(SCHEDULE_TASK_FN)], + Templates = [new($"{SCHEDULE_TASK_FN}.fn")] + } + }; + + utilities.AddRange(items); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs new file mode 100644 index 00000000..78b737a0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabService.cs @@ -0,0 +1,54 @@ +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + +using BotSharp.Abstraction.Repositories; +using BotSharp.Core.Infrastructures; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Core.Crontab.Services; + +/// +/// The Crontab service schedules distributed events based on the execution times provided by users. +/// In a scalable environment, distributed locks are used to ensure that each event is triggered only once. +/// +public class CrontabService : ICrontabService +{ + private readonly IServiceProvider _services; + private ILogger _logger; + + public CrontabService(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task> GetCrontable() + { + var repo = _services.GetRequiredService(); + var crontable = repo.GetCrontabItems(CrontabItemFilter.Empty()); + return crontable.Items.ToList(); + } + + public async Task ScheduledTimeArrived(CrontabItem item) + { + _logger.LogDebug($"ScheduledTimeArrived {item}"); + + await HookEmitter.Emit(_services, async hook => + await hook.OnCronTriggered(item) + ); + await Task.Delay(1000 * 10); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs new file mode 100644 index 00000000..5a48f58b --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Services/CrontabWatcher.cs @@ -0,0 +1,80 @@ +using BotSharp.Abstraction.Infrastructures; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NCrontab; + +namespace BotSharp.Core.Crontab.Services; + +public class CrontabWatcher : BackgroundService +{ + private readonly ILogger _logger; + private readonly IServiceProvider _services; + + public CrontabWatcher(IServiceProvider services, ILogger logger) + { + _logger = logger; + _services = services; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Crontab Watcher background service is running."); + + using (var scope = _services.CreateScope()) + { + var locker = scope.ServiceProvider.GetRequiredService(); + + while (!stoppingToken.IsCancellationRequested) + { + var delay = Task.Delay(1000, stoppingToken); + + await locker.LockAsync("CrontabWatcher", async () => + { + await RunCronChecker(scope.ServiceProvider); + }); + + await delay; + } + + _logger.LogWarning("Crontab Watcher background service is stopped."); + } + } + + private async Task RunCronChecker(IServiceProvider services) + { + var cron = services.GetRequiredService(); + var crons = await cron.GetCrontable(); + foreach (var item in crons) + { + try + { + // strip seconds from cron expression + item.Cron = string.Join(" ", item.Cron.Split(' ').TakeLast(5)); + var schedule = CrontabSchedule.Parse(item.Cron, new CrontabSchedule.ParseOptions + { + IncludingSeconds = false // Ensure you account for seconds + }); + + // Get the current time + var currentTime = DateTime.UtcNow; + + // Get the next occurrence from the schedule + var nextOccurrence = schedule.GetNextOccurrence(currentTime.AddSeconds(-1)); + + // Check if the current time matches the schedule + bool matches = currentTime >= nextOccurrence && currentTime < nextOccurrence.AddSeconds(1); + + if (matches) + { + _logger.LogDebug($"The current time matches the cron expression {item}"); + cron.ScheduledTimeArrived(item); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Error when running cron task ({item.ConversationId}, {item.Title}, {item.Cron}): {ex.Message}\r\n{ex.InnerException}"); + continue; + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core.Crontab/Using.cs b/src/Infrastructure/BotSharp.Core.Crontab/Using.cs new file mode 100644 index 00000000..0420faf7 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/Using.cs @@ -0,0 +1,13 @@ +global using System.Text.Json; + +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; + +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Crontab.Models; +global using BotSharp.Abstraction.Agents; +global using BotSharp.Abstraction.Plugins; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Functions; +global using BotSharp.Core.Crontab.Services; +global using BotSharp.Core.Crontab.Abstraction; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Crontab/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-crontab-schedule_task.json b/src/Infrastructure/BotSharp.Core.Crontab/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-crontab-schedule_task.json new file mode 100644 index 00000000..04131fe1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-crontab-schedule_task.json @@ -0,0 +1,48 @@ +{ + "name": "util-crontab-schedule_task", + "description": "Set up or cancel a scheduled task", + "parameters": { + "type": "object", + "properties": { + "cron_expression": { + "type": "string", + "description": "cron expression. Set value as empty if user wants to cancel schedule." + }, + "less_than_60_seconds": { + "type": "boolean", + "description": "whether schedule interval is less than 60 seconds" + }, + "title": { + "type": "string", + "description": "task short title" + }, + "description": { + "type": "string", + "description": "the task summary" + }, + "to_do_list": { + "type": "array", + "description": "task to do list, should include all key business entities", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "description": "task topic in 1-3 keywords related to the step" + }, + "script": { + "type": "string", + "description": "task related script, function with parameters or plain text" + }, + "language": { + "type": "string", + "enum": [ "function", "sql", "python", "text" ], + "description": "programming language script, function or plain text" + } + } + } + } + }, + "required": [ "cron_expression", "include_seconds", "title", "description", "to_do_list" ] + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Crontab/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-crontab-schedule_task.fn.liquid b/src/Infrastructure/BotSharp.Core.Crontab/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-crontab-schedule_task.fn.liquid new file mode 100644 index 00000000..17f9cc86 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Crontab/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-crontab-schedule_task.fn.liquid @@ -0,0 +1,2 @@ +Call util-crontab-schedule_task if user needs to set up a scheduled task with appropriate programming script and language type. +Set cron_expression as empty if user wants to cancel schedule. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj b/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj new file mode 100644 index 00000000..caef78b8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs new file mode 100644 index 00000000..14177437 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Core.Rules; + +public class RulesPlugin : IBotSharpPlugin +{ + public string Id => "0197c1bc-9ae6-4c56-a305-8a1b4095bebc"; + public string Name => "BotSharp Rules"; + public string Description => "Translates user-defined natural language rules into programmatic code and is responsible for executing these rules under user-specified conditions."; + public string IconUrl => "https://w7.pngwing.com/pngs/442/614/png-transparent-regulation-computer-icons-regulatory-compliance-medical-device-manufacturing-others-miscellaneous-blue-text-thumbnail.png"; + + public string[] AgentIds = + [ + BuiltInAgentId.RuleEncoder + ]; + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Using.cs b/src/Infrastructure/BotSharp.Core.Rules/Using.cs new file mode 100644 index 00000000..d982daae --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Using.cs @@ -0,0 +1,4 @@ +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Plugins; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj b/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj index 4b661c2a..b7f89568 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj +++ b/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs b/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs index efef5201..d3ace389 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs @@ -8,8 +8,10 @@ namespace BotSharp.Core.SideCar; public class BotSharpSideCarPlugin : IBotSharpPlugin { public string Id => "06e5a276-bba0-45af-9625-889267c341c9"; - public string Name => "Side Car"; - public string Description => "Provides side car for calling agent cluster in conversation"; + public string Name => "BotSharp SideCar"; + public string Description => "Provide side car pattern to to better handle Agent Cluster calls in the same conversation. Agent cluster is composed of multiple Routing Agents."; + public string? IconUrl => "https://icons.veryicon.com/png/128/internet--web/2022-alibaba-cloud-product-icon-cloud/aliyuncvc-cloud-video-conference.png"; + public SettingsMeta Settings => new SettingsMeta("SideCar"); public object GetNewSettingsInstance() => new SideCarSettings(); diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs index fda96b17..59087311 100644 --- a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs +++ b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs @@ -1,3 +1,21 @@ +/***************************************************************************** + Copyright 2024 Written by Jicheng Lu. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + +using BotSharp.Core.Infrastructures; + namespace BotSharp.Core.SideCar.Services; public class BotSharpConversationSideCar : IConversationSideCar @@ -62,9 +80,9 @@ public class BotSharpConversationSideCar : IConversationSideCar } public async Task SendMessage(string agentId, string text, - PostbackMessageModel? postback = null, List? states = null) + PostbackMessageModel? postback = null, List? states = null, List? dialogs = null) { - BeforeExecute(); + BeforeExecute(dialogs); var response = await InnerExecute(agentId, text, postback, states); AfterExecute(); return response; @@ -96,7 +114,7 @@ public class BotSharpConversationSideCar : IConversationSideCar return response; } - private void BeforeExecute() + private void BeforeExecute(List? dialogs) { enabled = true; var state = _services.GetRequiredService(); @@ -105,8 +123,8 @@ public class BotSharpConversationSideCar : IConversationSideCar var node = new ConversationContext { State = state.GetCurrentState(), - Dialogs = new(), - Breakpoints = new(), + Dialogs = dialogs ?? [], + Breakpoints = [], RecursiveCounter = routing.Context.GetRecursiveCounter(), RoutingStack = routing.Context.GetAgentStack() }; @@ -116,7 +134,7 @@ public class BotSharpConversationSideCar : IConversationSideCar state.ResetCurrentState(); routing.Context.ResetRecursiveCounter(); routing.Context.ResetAgentStack(); - + Utilities.ClearCache(); } private void AfterExecute() @@ -130,6 +148,7 @@ public class BotSharpConversationSideCar : IConversationSideCar state.SetCurrentState(node.State); routing.Context.SetRecursiveCounter(node.RecursiveCounter); routing.Context.SetAgentStack(node.RoutingStack); + Utilities.ClearCache(); enabled = false; } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index 2f51f1dd..b21772a1 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -1,7 +1,9 @@ using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Settings; +using BotSharp.Abstraction.Templating; using BotSharp.Abstraction.Users.Enums; +using BotSharp.Core.Agents.Hooks; using Microsoft.Extensions.Configuration; namespace BotSharp.Core.Agents; @@ -29,10 +31,13 @@ public class AgentPlugin : IBotSharpPlugin { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(provider => { var settingService = provider.GetRequiredService(); + var render = provider.GetRequiredService(); + render.Register(typeof(AgentSettings)); return settingService.Bind("Agent"); }); } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs b/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs new file mode 100644 index 00000000..65d454c8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs @@ -0,0 +1,83 @@ +namespace BotSharp.Core.Agents.Hooks; + +public class BasicAgentHook : AgentHookBase +{ + public override string SelfId => string.Empty; + + public BasicAgentHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override void OnAgentUtilityLoaded(Agent agent) + { + if (agent.Type == AgentType.Routing) return; + + var conv = _services.GetRequiredService(); + var isConvMode = conv.IsConversationMode(); + if (!isConvMode) return; + + agent.Functions ??= []; + agent.Utilities ??= []; + + var (functions, templates) = GetUtilityContent(agent); + + foreach (var fn in functions) + { + if (!agent.Functions.Any(x => x.Name.Equals(fn.Name, StringComparison.OrdinalIgnoreCase))) + { + agent.Functions.Add(fn); + } + } + + foreach (var prompt in templates) + { + agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; + } + } + + private (IEnumerable, IEnumerable) GetUtilityContent(Agent agent) + { + var db = _services.GetRequiredService(); + var (functionNames, templateNames) = GetUniqueContent(agent.Utilities); + + if (agent.MergeUtility) + { + var routing = _services.GetRequiredService(); + var entryAgentId = routing.EntryAgentId; + if (!string.IsNullOrEmpty(entryAgentId)) + { + var entryAgent = db.GetAgent(entryAgentId, basicsOnly: true); + var (fns, tps) = GetUniqueContent(entryAgent?.Utilities); + functionNames = functionNames.Concat(fns).Distinct().ToList(); + templateNames = templateNames.Concat(tps).Distinct().ToList(); + } + } + + var ua = db.GetAgent(BuiltInAgentId.UtilityAssistant); + var functions = ua?.Functions?.Where(x => functionNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))?.ToList() ?? []; + var templates = ua?.Templates?.Where(x => templateNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))?.Select(x => x.Content)?.ToList() ?? []; + return (functions, templates); + } + + private (IEnumerable, IEnumerable) GetUniqueContent(IEnumerable? utilities) + { + if (utilities.IsNullOrEmpty()) + { + return ([], []); + } + + var prefix = "util-"; + utilities = utilities?.Where(x => !string.IsNullOrEmpty(x.Name) && !x.Disabled)?.ToList() ?? []; + var functionNames = utilities.SelectMany(x => x.Functions) + .Where(x => !string.IsNullOrEmpty(x.Name) && x.Name.StartsWith(prefix)) + .Select(x => x.Name) + .Distinct().ToList(); + var templateNames = utilities.SelectMany(x => x.Templates) + .Where(x => !string.IsNullOrEmpty(x.Name) && x.Name.StartsWith(prefix)) + .Select(x => x.Name) + .Distinct().ToList(); + + return (functionNames, templateNames); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs index f0a864a2..0e9c2e3c 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs @@ -5,7 +5,7 @@ namespace BotSharp.Core.Agents.Services; public partial class AgentService { - public static ConcurrentDictionary> AgentParameterTypes = new(); + public static ConcurrentDictionary> AgentParameterTypes = new(); [MemoryCache(10 * 60, perInstanceCache: true)] public async Task LoadAgent(string id) @@ -67,6 +67,7 @@ public partial class AgentService hook.OnSamplesLoaded(agent.Samples); } + hook.OnAgentUtilityLoaded(agent); hook.OnAgentLoaded(agent); } @@ -105,14 +106,14 @@ public partial class AgentService { var agentId = agent.Id ?? agent.Name; if (AgentParameterTypes.ContainsKey(agentId)) return; - + AddOrUpdateRoutesParameters(agentId, agent.RoutingRules); AddOrUpdateFunctionsParameters(agentId, agent.Functions); } private void AddOrUpdateRoutesParameters(string agentId, List routingRules) { - if(!AgentParameterTypes.TryGetValue(agentId, out var parameterTypes)) + if (!AgentParameterTypes.TryGetValue(agentId, out var parameterTypes)) { parameterTypes = new(); } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs index e9617d17..ba76e310 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.Rendering.cs @@ -9,13 +9,16 @@ public partial class AgentService public string RenderedInstruction(Agent agent) { var render = _services.GetRequiredService(); - // update states var conv = _services.GetRequiredService(); + + // update states foreach (var t in conv.States.GetStates()) { agent.TemplateDict[t.Key] = t.Value; } - return render.Render(agent.Instruction, agent.TemplateDict); + + var res = render.Render(agent.Instruction, agent.TemplateDict); + return res; } public bool RenderFunction(Agent agent, FunctionDef def) @@ -108,16 +111,18 @@ public partial class AgentService public string RenderedTemplate(Agent agent, string templateName) { - // render liquid template - var render = _services.GetRequiredService(); - var template = agent.Templates.First(x => x.Name == templateName).Content; - // update states var conv = _services.GetRequiredService(); + var render = _services.GetRequiredService(); + + var template = agent.Templates.First(x => x.Name == templateName).Content; + + // update states foreach (var t in conv.States.GetStates()) { agent.TemplateDict[t.Key] = t.Value; } + // render liquid template var content = render.Render(template, agent.TemplateDict); HookEmitter.Emit(_services, async hook => @@ -126,4 +131,4 @@ public partial class AgentService return content; } -} +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 18afb27f..ffea8f8d 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Repositories.Enums; -using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; using System.IO; @@ -28,16 +27,18 @@ public partial class AgentService record.Description = agent.Description ?? string.Empty; record.IsPublic = agent.IsPublic; record.Disabled = agent.Disabled; + record.MergeUtility = agent.MergeUtility; + record.MaxMessageCount = agent.MaxMessageCount; record.Type = agent.Type; - record.Profiles = agent.Profiles ?? new List(); - record.RoutingRules = agent.RoutingRules ?? new List(); + record.Profiles = agent.Profiles ?? []; + record.RoutingRules = agent.RoutingRules ?? []; record.Instruction = agent.Instruction ?? string.Empty; - record.ChannelInstructions = agent.ChannelInstructions ?? new List(); - record.Functions = agent.Functions ?? new List(); - record.Templates = agent.Templates ?? new List(); - record.Responses = agent.Responses ?? new List(); - record.Samples = agent.Samples ?? new List(); - record.Utilities = agent.Utilities ?? new List(); + record.ChannelInstructions = agent.ChannelInstructions ?? []; + record.Functions = agent.Functions ?? []; + record.Templates = agent.Templates ?? []; + record.Responses = agent.Responses ?? []; + record.Samples = agent.Samples ?? []; + record.Utilities = agent.Utilities ?? []; if (agent.LlmConfig != null && !agent.LlmConfig.IsInherit) { record.LlmConfig = agent.LlmConfig; @@ -90,6 +91,7 @@ public partial class AgentService .SetDescription(foundAgent.Description) .SetIsPublic(foundAgent.IsPublic) .SetDisabled(foundAgent.Disabled) + .SetMergeUtility(foundAgent.MergeUtility) .SetAgentType(foundAgent.Type) .SetProfiles(foundAgent.Profiles) .SetRoutingRules(foundAgent.RoutingRules) diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index a48201d8..1dd331af 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -57,14 +57,14 @@ public partial class AgentService : IAgentService return userAgents; } - public IEnumerable GetAgentUtilities() + public IEnumerable GetAgentUtilityOptions() { - var utilities = new List(); + var utilities = new List(); var hooks = _services.GetServices(); foreach (var hook in hooks) { hook.AddUtilities(utilities); } - return utilities.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().OrderBy(x => x).ToList(); + return utilities.Where(x => !string.IsNullOrWhiteSpace(x.Name)).OrderBy(x => x.Name).ToList(); } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 32987e69..1c9955aa 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -59,6 +59,11 @@ + + + + + @@ -73,10 +78,6 @@ - - - - @@ -120,16 +121,19 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest + + PreserveNewest @@ -186,7 +190,7 @@ - + diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index a60fb4fd..af8a9324 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -12,6 +12,8 @@ using BotSharp.Core.Processors; using StackExchange.Redis; using BotSharp.Core.Infrastructures.Events; using BotSharp.Core.Roles.Services; +using BotSharp.Abstraction.Templating; +using BotSharp.Core.Templating; namespace BotSharp.Core; @@ -23,7 +25,9 @@ public static class BotSharpCoreExtensions config.Bind("Interpreter", interpreterSettings); services.AddSingleton(x => interpreterSettings); - services.AddSingleton(); + services.AddSingleton(); + // Register template render + services.AddSingleton(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs index a9be6fd2..09936dec 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/ConversationPlugin.cs @@ -1,13 +1,13 @@ using BotSharp.Abstraction.Google.Settings; using BotSharp.Abstraction.Instructs; using BotSharp.Abstraction.Messaging; +using BotSharp.Abstraction.Planning; using BotSharp.Abstraction.Plugins.Models; -using BotSharp.Abstraction.Routing.Planning; using BotSharp.Abstraction.Settings; using BotSharp.Abstraction.Templating; using BotSharp.Core.Instructs; using BotSharp.Core.Messaging; -using BotSharp.Core.Routing.Planning; +using BotSharp.Core.Routing.Reasoning; using BotSharp.Core.Templating; using BotSharp.Core.Translation; using Microsoft.Extensions.Configuration; @@ -30,6 +30,8 @@ public class ConversationPlugin : IBotSharpPlugin services.AddScoped(provider => { var settingService = provider.GetRequiredService(); + var render = provider.GetRequiredService(); + render.Register(typeof(ConversationSetting)); return settingService.Bind("Conversation"); }); @@ -48,8 +50,6 @@ public class ConversationPlugin : IBotSharpPlugin // Rich content messaging services.AddScoped(); - // Register template render - services.AddSingleton(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 375cf3e6..f60d32cf 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -28,7 +28,7 @@ public partial class ConversationService var dialogs = conv.GetDialogHistory(); var statistics = _services.GetRequiredService(); - var hooks = _services.GetServices().ToList(); + var hookProvider = _services.GetRequiredService(); RoleDialogModel response = message; bool stopCompletion = false; @@ -44,9 +44,7 @@ public partial class ConversationService message.Payload = replyMessage.Payload; } - // Before chat completion hook - hooks = ReOrderConversationHooks(hooks); - foreach (var hook in hooks) + foreach (var hook in hookProvider.HooksOrderByPriority) { hook.SetAgent(agent) .SetConversation(conversation); @@ -173,18 +171,4 @@ public partial class ConversationService // Add to dialog history _storage.Append(_conversationId, response); } - - private List ReOrderConversationHooks(List hooks) - { - var target = "ChatHubConversationHook"; - var chathub = hooks.FirstOrDefault(x => x.GetType().Name == target); - var otherHooks = hooks.Where(x => x.GetType().Name != target).ToList(); - - if (chathub != null) - { - var newHooks = new List { chathub }.Concat(otherHooks); - return newHooks.ToList(); - } - return hooks; - } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs index ccc074e8..1a75d717 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.TruncateMessage.cs @@ -9,7 +9,7 @@ public partial class ConversationService : IConversationService var deleteMessageIds = db.TruncateConversation(conversationId, messageId, cleanLog: true); fileStorage.DeleteMessageFiles(conversationId, deleteMessageIds, messageId, newMessageId); - var hooks = _services.GetServices().ToList(); + var hooks = _services.GetServices(); foreach (var hook in hooks) { await hook.OnMessageDeleted(conversationId, messageId); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs index 8f88f44f..75a0bbfb 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.UpdateBreakpoint.cs @@ -31,9 +31,9 @@ public partial class ConversationService : IConversationService states.CleanStates(excludedStates); } - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); + var hooks = _services + .GetRequiredService() + .HooksOrderByPriority; // Before executing functions foreach (var hook in hooks) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index f6326a7b..7e498afe 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -51,6 +51,14 @@ public partial class ConversationService : IConversationService return conversation; } + public async Task UpdateConversationTitleAlias(string id, string titleAlias) + { + var db = _services.GetRequiredService(); + db.UpdateConversationTitleAlias(id, titleAlias); + var conversation = db.GetConversation(id); + return conversation; + } + public async Task UpdateConversationTags(string conversationId, List tags) { var db = _services.GetRequiredService(); @@ -103,7 +111,8 @@ public partial class ConversationService : IConversationService db.CreateNewConversation(record); - var hooks = _services.GetServices().ToList(); + var hooks = _services.GetServices(); + foreach (var hook in hooks) { // If user connect agent first time @@ -153,7 +162,10 @@ public partial class ConversationService : IConversationService } } - return dialogs.TakeLast(lastCount).ToList(); + var agentMsgCount = GetAgentMessageCount(); + var count = agentMsgCount.HasValue && agentMsgCount.Value > 0 ? agentMsgCount.Value : lastCount; + + return dialogs.TakeLast(count).ToList(); } public void SetConversationId(string conversationId, List states, bool isReadOnly = false) @@ -173,12 +185,14 @@ public partial class ConversationService : IConversationService var state = _services.GetRequiredService(); var channel = state.GetState("channel"); var channelId = state.GetState("channel_id"); + var userId = state.GetState("current_user_id"); var sess = new Conversation { Id = _conversationId, Channel = channel, ChannelId = channelId, - AgentId = agentId + AgentId = agentId, + UserId = userId, }; converation = await NewConversation(sess); } @@ -190,4 +204,16 @@ public partial class ConversationService : IConversationService { return !string.IsNullOrWhiteSpace(_conversationId); } + + + private int? GetAgentMessageCount() + { + var db = _services.GetRequiredService(); + var routingCtx = _services.GetRequiredService(); + + if (string.IsNullOrEmpty(routingCtx.EntryAgentId)) return null; + + var agent = db.GetAgent(routingCtx.EntryAgentId, basicsOnly: true); + return agent?.MaxMessageCount; + } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 8ee4c662..eedcdf9a 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -1,5 +1,20 @@ +/***************************************************************************** + Copyright 2024 Written by Jicheng Lu. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + using BotSharp.Abstraction.Conversations.Enums; -using BotSharp.Abstraction.Users.Enums; namespace BotSharp.Core.Conversations.Services; @@ -21,13 +36,14 @@ public class ConversationStateService : IConversationStateService, IDisposable /// private ConversationState _historyStates; - public ConversationStateService(ILogger logger, + public ConversationStateService( IServiceProvider services, - IBotSharpRepository db) + IBotSharpRepository db, + ILogger logger) { - _logger = logger; _services = services; _db = db; + _logger = logger; _curStates = new ConversationState(); _historyStates = new ConversationState(); } @@ -125,8 +141,13 @@ public class ConversationStateService : IConversationStateService, IDisposable var curMsgId = routingCtx.MessageId; _historyStates = _db.GetConversationStates(conversationId); + + var endNodes = new Dictionary(); + + if (_historyStates.IsNullOrEmpty()) return endNodes; + var dialogs = _db.GetConversationDialogs(conversationId); - var userDialogs = dialogs.Where(x => x.MetaData?.Role == AgentRole.User || x.MetaData?.Role == UserRole.User) + var userDialogs = dialogs.Where(x => x.MetaData?.Role == AgentRole.User) .GroupBy(x => x.MetaData?.MessageId) .Select(g => g.First()) .OrderBy(x => x.MetaData?.CreateTime) @@ -134,9 +155,6 @@ public class ConversationStateService : IConversationStateService, IDisposable var curMsgIndex = userDialogs.FindIndex(x => !string.IsNullOrEmpty(curMsgId) && x.MetaData?.MessageId == curMsgId); curMsgIndex = curMsgIndex < 0 ? userDialogs.Count() : curMsgIndex; - var endNodes = new Dictionary(); - if (_historyStates.IsNullOrEmpty()) return endNodes; - foreach (var state in _historyStates) { var key = state.Key; diff --git a/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs b/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs index bfc0c1ec..b315b683 100644 --- a/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs +++ b/src/Infrastructure/BotSharp.Core/Evaluations/EvaluationConversationHook.cs @@ -15,45 +15,45 @@ public class EvaluationConversationHook : ConversationHookBase public override Task OnMessageReceived(RoleDialogModel message) { - if (_conversation != null && _convSettings.EnableExecutionLog) + if (Conversation != null && _convSettings.EnableExecutionLog) { - _logger.Append(_conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}"); + _logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}"); } return base.OnMessageReceived(message); } public override Task OnFunctionExecuted(RoleDialogModel message) { - if (_conversation != null && _convSettings.EnableExecutionLog) + if (Conversation != null && _convSettings.EnableExecutionLog) { - _logger.Append(_conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.FunctionName}({message.FunctionArgs}) => {message.Content}"); + _logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.FunctionName}({message.FunctionArgs}) => {message.Content}"); } return base.OnFunctionExecuted(message); } public override Task OnResponseGenerated(RoleDialogModel message) { - if (_conversation != null && _convSettings.EnableExecutionLog) + if (Conversation != null && _convSettings.EnableExecutionLog) { - _logger.Append(_conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}"); - } + _logger.Append(Conversation.Id, $"[{DateTime.Now}] {message.Role}: {message.Content}"); + } return base.OnResponseGenerated(message); } public override Task OnHumanInterventionNeeded(RoleDialogModel message) { - if (_conversation != null && _convSettings.EnableExecutionLog) + if (Conversation != null && _convSettings.EnableExecutionLog) { - _logger.Append(_conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})"); + _logger.Append(Conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})"); } return base.OnHumanInterventionNeeded(message); } public override Task OnConversationEnding(RoleDialogModel message) { - if (_conversation != null && _convSettings.EnableExecutionLog) + if (Conversation != null && _convSettings.EnableExecutionLog) { - _logger.Append(_conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})"); + _logger.Append(Conversation.Id, $"[{DateTime.Now}] {AgentRole.Function}: trigger_event({{\"event\": \"{message.FunctionName}\"}})"); } return base.OnConversationEnding(message); } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs index 32c7af52..48ff193f 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs @@ -1,9 +1,10 @@ +using BotSharp.Abstraction.Infrastructures; using Medallion.Threading.Redis; using StackExchange.Redis; namespace BotSharp.Core.Infrastructures; -public class DistributedLocker +public class DistributedLocker : IDistributedLocker { private readonly IConnectionMultiplexer _redis; private readonly ILogger _logger; @@ -14,7 +15,7 @@ public class DistributedLocker _logger = logger; } - public async Task Lock(string resource, Func> action, int timeoutInSeconds = 30) + public async Task LockAsync(string resource, Func action, int timeoutInSeconds = 30) { var timeout = TimeSpan.FromSeconds(timeoutInSeconds); @@ -24,9 +25,11 @@ public class DistributedLocker if (handle == null) { _logger.LogWarning($"Acquire lock for {resource} failed due to after {timeout}s timeout."); + return false; } - return await action(); + await action(); + return true; } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs index f6daa59d..89de2453 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisPublisher.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using StackExchange.Redis; namespace BotSharp.Core.Infrastructures.Events; @@ -20,14 +21,65 @@ public class RedisPublisher : IEventPublisher await _subscriber.PublishAsync(channel, message); } - public async Task PublishAsync(string channel, string message) + public async Task PublishAsync(string channel, string message, EventPriority? priority = null) { var db = _redis.GetDatabase(); + + // convert to apporiate channel by priority + if (priority != null) + { + channel = $"{channel}-{priority}"; + } + + if (CheckMessageExists(db, channel, "message", message)) + { + _logger.LogError($"The message already exists {channel} {message}"); + return null; + } + // Add a message to the stream, keeping only the latest 1 million messages - await db.StreamAddAsync(channel, "message", message, + var messageId = await db.StreamAddAsync(channel, + [ + new NameValueEntry("message", message), + new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")) + ], maxLength: 1000 * 10000); - _logger.LogInformation($"Published message {channel} {message}"); + _logger.LogInformation($"Published message {channel} {message} ({messageId})"); + + return messageId; + } + + private bool CheckMessageExists(IDatabase db, string channel, string fieldName, string desiredValue) + { + // Define the range to fetch all messages + RedisValue start = "-"; // Start from the smallest ID + RedisValue end = "+"; // End at the largest ID + int count = 10; // Number of messages to retrieve + + // Fetch the latest 10 messages + var streamEntries = db.StreamRange(channel, start, end, count, Order.Descending); + + if (streamEntries.Length == 0) + { + return false; + } + + // Check if any message contains the specific field value + bool exists = streamEntries.Any(entry => + { + // Each entry contains a collection of Name-Value pairs + foreach (var nameValue in entry.Values) + { + if (nameValue.Name == fieldName && nameValue.Value == desiredValue) + { + return true; + } + } + return false; + }); + + return exists; } public async Task ReDispatchAsync(string channel, int count = 10, string order = "asc") @@ -41,7 +93,10 @@ public class RedisPublisher : IEventPublisher try { - var messageId = await db.StreamAddAsync(channel, "message", entry.Values[0].Value); + var messageId = await db.StreamAddAsync(channel, [ + new NameValueEntry("message", entry.Values[0].Value), + new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")) + ]); _logger.LogWarning($"ReDispatched message: {channel} {entry.Values[0].Value} ({messageId})"); @@ -50,7 +105,78 @@ public class RedisPublisher : IEventPublisher } catch (Exception ex) { - _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}"); + _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}"); + } + } + } + + public async Task ReDispatchPendingAsync(string channel, string group, int count = 10) + { + var db = _redis.GetDatabase(); + + // Step 1: Fetch pending messages using XPending + try + { + var pendingInfo = await db.StreamPendingAsync(channel, group); + + if (pendingInfo.PendingMessageCount == 0) + { + Console.WriteLine("No pending messages found."); + return; + } + + // Step 2: Fetch pending message IDs + List pendingMessages = new List(); + foreach (var consumer in pendingInfo.Consumers) + { + var pendingMessageIds = await db.StreamPendingMessagesAsync(channel, group, count: consumer.PendingMessageCount, consumerName: consumer.Name); + var messageIds = pendingMessageIds.Select(x => x.MessageId).ToArray(); + // Step 3: Use XClaim to fetch the actual message + var claimedMessages = await db.StreamClaimAsync(channel, group, consumer.Name, minIdleTimeInMs: 60 * 1000, messageIds); + pendingMessages.AddRange(claimedMessages); + + await db.StreamAcknowledgeAsync(channel, group, messageIds); + } + + // Step 4: Process the messages + foreach (var message in pendingMessages) + { + /*if (message.IsNull) + { + await db.StreamAcknowledgeAsync(channel, group, [message.Id]); + } + else + { + var messageId = await db.StreamAddAsync(channel, "message", message.Values[0].Value); + _logger.LogWarning($"ReDispatched message: {channel} {message.Values[0].Value} ({messageId})"); + await db.StreamDeleteAsync(channel, [messageId]); + }*/ + } + } + catch (RedisException ex) + { + Console.WriteLine($"Redis error: {ex.Message}"); + } + } + + public async Task RemoveAsync(string channel, int count = 10) + { + var db = _redis.GetDatabase(); + + var entries = await db.StreamRangeAsync(channel, "-", "+", count: count, messageOrder: Order.Ascending); + foreach (var entry in entries) + { + _logger.LogInformation($"Fetched message: {channel} {entry.Values[0].Value} ({entry.Id})"); + + try + { + await db.StreamDeleteAsync(channel, [entry.Id]); + + _logger.LogWarning($"Deleted message: {channel} {entry.Values[0].Value} ({entry.Id})"); + } + catch (Exception ex) + { + _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}"); } } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs index 726b9358..9a3c9982 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Events/RedisSubscriber.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Infrastructures.Enums; using StackExchange.Redis; namespace BotSharp.Core.Infrastructures.Events; @@ -24,10 +25,86 @@ public class RedisSubscriber : IEventSubscriber }); } - public async Task SubscribeAsync(string channel, string group, Func received) + public async Task SubscribeAsync(string channel, string group, int? port, bool priorityEnabled, + Func received, + CancellationToken? stoppingToken = null) { var db = _redis.GetDatabase(); + if (priorityEnabled) + { + await CreateConsumerGroup(db, $"{channel}-{EventPriority.Low}", group); + await CreateConsumerGroup(db, $"{channel}-{EventPriority.Medium}", group); + await CreateConsumerGroup(db, $"{channel}-{EventPriority.High}", group); + } + else + { + await CreateConsumerGroup(db, channel, group); + } + + var consumer = Environment.MachineName; + if (port.HasValue) + { + consumer += $"-{port}"; + } + + while (true) + { + await Task.Delay(100); + + if (stoppingToken.HasValue && stoppingToken.Value.IsCancellationRequested) + { + _logger.LogInformation($"Stopping consumer channel & group: [{channel}, {group}]"); + break; + } + + if (priorityEnabled) + { + if (await HandleGroupMessage(db, $"{channel}-{EventPriority.High}", group, consumer, received) > 0) + { + continue; + } + + if (await HandleGroupMessage(db, $"{channel}-{EventPriority.Medium}", group, consumer, received) > 0) + { + continue; + } + + await HandleGroupMessage(db, $"{channel}-{EventPriority.Low}", group, consumer, received); + } + else + { + await HandleGroupMessage(db, channel, group, consumer, received); + } + } + } + + private async Task HandleGroupMessage(IDatabase db, string channel, string group, string consumer, Func received) + { + var entries = await db.StreamReadGroupAsync(channel, group, consumer, count: 1); + foreach (var entry in entries) + { + _logger.LogInformation($"Consumer {Environment.MachineName} received: {channel} {entry.Values[0].Value}"); + await db.StreamAcknowledgeAsync(channel, group, entry.Id); + + try + { + await received(channel, entry.Values[0].Value); + + // Optionally delete the message to save space + await db.StreamDeleteAsync(channel, [entry.Id]); + } + catch (Exception ex) + { + _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}\r\n{ex}"); + } + } + + return entries.Length; + } + + private async Task CreateConsumerGroup(IDatabase db, string channel, string group) + { // Create the consumer group if it doesn't exist try { @@ -43,33 +120,5 @@ public class RedisSubscriber : IEventSubscriber _logger.LogError($"Error creating consumer group: '{group}' {ex.Message}"); throw; } - - while (true) - { - var entries = await db.StreamReadGroupAsync(channel, group, Environment.MachineName, count: 1); - foreach (var entry in entries) - { - _logger.LogInformation($"Consumer {Environment.MachineName} received: {channel} {entry.Values[0].Value}"); - - try - { - await received(channel, entry.Values[0].Value); - - // Optionally delete the message to save space - await db.StreamDeleteAsync(channel, [entry.Id]); - } - catch (Exception ex) - { - _logger.LogError($"Error processing message: {ex.Message}, event id: {channel} {entry.Id}"); - } - finally - { - await db.StreamAcknowledgeAsync(channel, group, entry.Id); - } - } - - await Task.Delay(Random.Shared.Next(1, 11) * 100); - } - } } diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs index 943386e1..454ed840 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/HookEmitter.cs @@ -15,7 +15,7 @@ public static class HookEmitter { try { - logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})"); + logger.LogDebug($"Emit hook action on {action.Method.Name}({hook.GetType().Name})"); action(hook); if (option.OnlyOnce) @@ -43,7 +43,7 @@ public static class HookEmitter { try { - logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})"); + logger.LogDebug($"Emit hook action on {action.Method.Name}({hook.GetType().Name})"); await action(hook); if (option.OnlyOnce) diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs index 4cb26888..db5a9a7d 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/Utilities.cs @@ -23,17 +23,23 @@ public static class Utilities var data = sha256.ComputeHash(Encoding.UTF8.GetBytes(text)); var sb = new StringBuilder(); - foreach(var c in data) + foreach (var c in data) { sb.Append(c.ToString("x2")); } return sb.ToString(); } - public static (string, string) SplitAsTuple(this string str, string sep) + public static (string, string, string) SplitAsTuple(this string str, string sep) { var splits = str.Split(sep); - return (splits[0], splits[1]); + + if (splits.Length == 2 || string.IsNullOrWhiteSpace(splits[2])) + { + return (splits[0], splits[1], "CN"); + } + + return (splits[0], splits[1], splits[2]); } /// @@ -47,4 +53,36 @@ public static class Utilities memcache.Compact(100); } } + + public static string HideMiddleDigits(string input, bool isEmail = false) + { + if (string.IsNullOrWhiteSpace(input)) + { + return input; + } + + if (isEmail) + { + int atIndex = input.IndexOf('@'); + if (atIndex > 1) + { + string localPart = input.Substring(0, atIndex); + if (localPart.Length > 2) + { + string maskedLocalPart = $"{localPart[0]}{new string('*', localPart.Length - 2)}{localPart[^1]}"; + return $"{maskedLocalPart}@{input.Substring(atIndex + 1)}"; + } + } + } + else + { + if (input.Length > 6) + { + return $"{input.Substring(0, 3)}{new string('*', input.Length - 6)}{input.Substring(input.Length - 3)}"; + } + } + + return input; + } + } diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index d2c519eb..719f3177 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -16,7 +16,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository #endregion #region Agent - public Agent GetAgent(string agentId) + public Agent GetAgent(string agentId, bool basicsOnly = false) => throw new NotImplementedException(); public List GetAgents(AgentFilter filter) @@ -105,6 +105,8 @@ public class BotSharpDbContext : Database, IBotSharpRepository public void UpdateConversationTitle(string conversationId, string title) => throw new NotImplementedException(); + public void UpdateConversationTitleAlias(string conversationId, string titleAlias) + => throw new NotImplementedException(); public bool UpdateConversationTags(string conversationId, List tags) => throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index 7f545527..a838870f 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -55,7 +55,10 @@ namespace BotSharp.Core.Repository UpdateAgentLlmConfig(agent.Id, agent.LlmConfig); break; case AgentField.Utility: - UpdateAgentUtilities(agent.Id, agent.Utilities); + UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities); + break; + case AgentField.MaxMessageCount: + UpdateAgentMaxMessageCount(agent.Id, agent.MaxMessageCount); break; case AgentField.All: UpdateAgentAllFields(agent); @@ -151,13 +154,14 @@ namespace BotSharp.Core.Repository File.WriteAllText(agentFile, json); } - private void UpdateAgentUtilities(string agentId, List utilities) + private void UpdateAgentUtilities(string agentId, bool mergeUtility, List utilities) { if (utilities == null) return; var (agent, agentFile) = GetAgentFromFile(agentId); if (agent == null) return; + agent.MergeUtility = mergeUtility; agent.Utilities = utilities; agent.UpdatedDateTime = DateTime.UtcNow; var json = JsonSerializer.Serialize(agent, _options); @@ -282,6 +286,17 @@ namespace BotSharp.Core.Repository File.WriteAllText(agentFile, json); } + private void UpdateAgentMaxMessageCount(string agentId, int? maxMessageCount) + { + var (agent, agentFile) = GetAgentFromFile(agentId); + if (agent == null) return; + + agent.MaxMessageCount = maxMessageCount; + agent.UpdatedDateTime = DateTime.UtcNow; + var json = JsonSerializer.Serialize(agent, _options); + File.WriteAllText(agentFile, json); + } + private void UpdateAgentAllFields(Agent inputAgent) { var (agent, agentFile) = GetAgentFromFile(inputAgent.Id); @@ -291,11 +306,13 @@ namespace BotSharp.Core.Repository agent.Description = inputAgent.Description; agent.IsPublic = inputAgent.IsPublic; agent.Disabled = inputAgent.Disabled; + agent.MergeUtility = inputAgent.MergeUtility; agent.Type = inputAgent.Type; agent.Profiles = inputAgent.Profiles; agent.Utilities = inputAgent.Utilities; agent.RoutingRules = inputAgent.RoutingRules; agent.LlmConfig = inputAgent.LlmConfig; + agent.MaxMessageCount = inputAgent.MaxMessageCount; agent.UpdatedDateTime = DateTime.UtcNow; var json = JsonSerializer.Serialize(agent, _options); File.WriteAllText(agentFile, json); @@ -327,7 +344,7 @@ namespace BotSharp.Core.Repository return responses; } - public Agent? GetAgent(string agentId) + public Agent? GetAgent(string agentId, bool basicsOnly = false) { var agentDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir); var dir = Directory.GetDirectories(agentDir).FirstOrDefault(x => x.Split(Path.DirectorySeparatorChar).Last() == agentId); @@ -340,6 +357,8 @@ namespace BotSharp.Core.Repository var record = JsonSerializer.Deserialize(json, _options); if (record == null) return null; + if (basicsOnly) return record; + var (defaultInstruction, channelInstructions) = FetchInstructions(dir); var functions = FetchFunctions(dir); var samples = FetchSamples(dir); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index fd13936c..34345271 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -134,6 +134,22 @@ namespace BotSharp.Core.Repository } } } + public void UpdateConversationTitleAlias(string conversationId, string titleAlias) + { + var convDir = FindConversationDirectory(conversationId); + if (!string.IsNullOrEmpty(convDir)) + { + var convFile = Path.Combine(convDir, CONVERSATION_FILE); + var content = File.ReadAllText(convFile); + var record = JsonSerializer.Deserialize(content, _options); + if (record != null) + { + record.TitleAlias = titleAlias; + record.UpdatedTime = DateTime.UtcNow; + File.WriteAllText(convFile, JsonSerializer.Serialize(record, _options)); + } + } + } public bool UpdateConversationTags(string conversationId, List tags) { @@ -356,6 +372,10 @@ namespace BotSharp.Core.Repository { matched = matched && record.Title.Contains(filter.Title); } + if (filter?.TitleAlias != null) + { + matched = matched && record.TitleAlias.Contains(filter.TitleAlias); + } if (filter?.AgentId != null) { matched = matched && record.AgentId == filter.AgentId; diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs new file mode 100644 index 00000000..5d3eb865 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Crontab.cs @@ -0,0 +1,117 @@ +using System.IO; + +namespace BotSharp.Core.Repository; + +public partial class FileRepository +{ + public bool UpsertCrontabItem(CrontabItem cron) + { + if (cron == null || string.IsNullOrWhiteSpace(cron.ConversationId)) + { + return false; + } + + try + { + var baseDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, cron.ConversationId); + if (!Directory.Exists(baseDir)) + { + return false; + } + + var cronFile = Path.Combine(baseDir, CRON_FILE); + var json = JsonSerializer.Serialize(cron, _options); + File.WriteAllText(cronFile, json); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error when saving crontab item: {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + public bool DeleteCrontabItem(string conversationId) + { + if (string.IsNullOrWhiteSpace(conversationId)) + { + return false; + } + + try + { + var baseDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir, conversationId); + if (!Directory.Exists(baseDir)) + { + return false; + } + + var cronFile = Path.Combine(baseDir, CRON_FILE); + if (!File.Exists(cronFile)) + { + return false; + } + + File.Delete(cronFile); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error when deleting crontab item (${conversationId}): {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + + public PagedItems GetCrontabItems(CrontabItemFilter filter) + { + + if (filter == null) + { + filter = CrontabItemFilter.Empty(); + } + + var records = new List(); + var baseDir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir); + + if (!Directory.Exists(baseDir)) + { + Directory.CreateDirectory(baseDir); + } + + var totalDirs = Directory.GetDirectories(baseDir); + foreach (var d in totalDirs) + { + var file = Path.Combine(d, CRON_FILE); + if (!File.Exists(file)) continue; + + var json = File.ReadAllText(file); + var record = JsonSerializer.Deserialize(json, _options); + if (record == null) continue; + + var matched = true; + if (filter?.AgentIds != null) + { + matched = matched && filter.AgentIds.Contains(record.AgentId); + } + if (filter?.ConversationIds != null) + { + matched = matched && filter.ConversationIds.Contains(record.ConversationId); + } + if (filter?.UserIds != null) + { + matched = matched && filter.UserIds.Contains(record.UserId); + } + + if (!matched) continue; + + records.Add(record); + } + + return new PagedItems + { + Items = records.OrderByDescending(x => x.CreatedTime).Skip(filter.Offset).Take(filter.Size), + Count = records.Count(), + }; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index 9600bbc8..9d5b42be 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -11,9 +11,21 @@ public partial class FileRepository return Users.FirstOrDefault(x => x.Email == email.ToLower()); } - public User? GetUserByPhone(string phone) + public User? GetUserByPhone(string phone, string? type = UserType.Client, string regionCode = "CN") { - return Users.FirstOrDefault(x => x.Phone == phone); + var query = Users.Where(x => x.Phone == phone); + + if (!string.IsNullOrEmpty(type)) + { + query = query.Where(x => x.Type == type); + } + + if (!string.IsNullOrEmpty(regionCode)) + { + query = query.Where(x => x.RegionCode == regionCode); + } + + return query.FirstOrDefault(); } public User? GetAffiliateUserByPhone(string phone) @@ -41,6 +53,11 @@ public partial class FileRepository return Users.FirstOrDefault(x => x.UserName == userName.ToLower()); } + public Dashboard? GetDashboard(string id = null) + { + return Dashboards.FirstOrDefault(); + } + public void CreateUser(User user) { var userId = Guid.NewGuid().ToString(); @@ -191,4 +208,68 @@ public partial class FileRepository _users = []; return true; } + + public void AddDashboardConversation(string userId, string conversationId) + { + var user = GetUserById(userId); + if (user == null) return; + + // one user only has one dashboard currently + var dash = Dashboards.FirstOrDefault(); + dash ??= new(); + var existingConv = dash.ConversationList.FirstOrDefault(x => string.Equals(x.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase)); + if (existingConv != null) return; + + var dashconv = new DashboardConversation + { + Id = Guid.NewGuid().ToString(), + ConversationId = conversationId + }; + + dash.ConversationList.Add(dashconv); + + var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, userId); + var path = Path.Combine(dir, DASHBOARD_FILE); + File.WriteAllText(path, JsonSerializer.Serialize(dash, _options)); + } + + public void RemoveDashboardConversation(string userId, string conversationId) + { + var user = GetUserById(userId); + if (user == null) return; + + // one user only has one dashboard currently + var dash = Dashboards.FirstOrDefault(); + if (dash == null) return; + + var dashconv = dash.ConversationList.FirstOrDefault( + c => string.Equals(c.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase)); + if (dashconv == null) return; + + dash.ConversationList.Remove(dashconv); + + var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, userId); + var path = Path.Combine(dir, DASHBOARD_FILE); + File.WriteAllText(path, JsonSerializer.Serialize(dash, _options)); + } + + public void UpdateDashboardConversation(string userId, DashboardConversation dashConv) + { + var user = GetUserById(userId); + if (user == null) return; + + // one user only has one dashboard currently + var dash = Dashboards.FirstOrDefault(); + if (dash == null) return; + + var curIdx = dash.ConversationList.ToList().FindIndex( + x => string.Equals(x.ConversationId, dashConv.ConversationId, StringComparison.OrdinalIgnoreCase)); + if (curIdx < 0) return; + + dash.ConversationList[curIdx] = dashConv; + + var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, userId); + var path = Path.Combine(dir, DASHBOARD_FILE); + File.WriteAllText(path, JsonSerializer.Serialize(dash, _options)); + } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 0edcb699..6a718d65 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -22,6 +22,7 @@ public partial class FileRepository : IBotSharpRepository private const string AGENT_FILE = "agent.json"; private const string AGENT_INSTRUCTION_FILE = "instruction"; private const string AGENT_SAMPLES_FILE = "samples.txt"; + private const string DASHBOARD_FILE = "dashboard.json"; private const string AGENT_INSTRUCTIONS_FOLDER = "instructions"; private const string AGENT_FUNCTIONS_FOLDER = "functions"; private const string AGENT_TEMPLATES_FOLDER = "templates"; @@ -54,6 +55,8 @@ public partial class FileRepository : IBotSharpRepository private const string PLUGIN_CONFIG_FILE = "config.json"; private const string STATS_FILE = "stats.json"; + private const string CRON_FILE = "cron.json"; + public FileRepository( IServiceProvider services, BotSharpDatabaseSettings dbSettings, @@ -83,10 +86,10 @@ public partial class FileRepository : IBotSharpRepository private List _roles = new List(); private List _users = new List(); + private List _dashboards = []; private List _agents = new List(); private List _roleAgents = new List(); private List _userAgents = new List(); - private List _conversations = new List(); private PluginConfig? _pluginConfig = null; private IQueryable Roles @@ -170,6 +173,36 @@ public partial class FileRepository : IBotSharpRepository } } + private IQueryable Dashboards + { + get + { + if (!_dashboards.IsNullOrEmpty()) + { + return _dashboards.AsQueryable(); + } + + var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER); + _dashboards = []; + if (Directory.Exists(dir)) + { + foreach (var d in Directory.GetDirectories(dir)) + { + var dashboardFile = Path.Combine(d, DASHBOARD_FILE); + if (!Directory.Exists(d) || !File.Exists(dashboardFile)) + continue; + + var json = File.ReadAllText(dashboardFile); + var dash = JsonSerializer.Deserialize(json, _options); + + if (dash == null) continue; + _dashboards.Add(dash); + } + } + return _dashboards.AsQueryable(); + } + } + private IQueryable Agents { get diff --git a/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs b/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs index 8cd7935b..9636fe18 100644 --- a/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs +++ b/src/Infrastructure/BotSharp.Core/Roles/Services/RoleService.cs @@ -39,7 +39,15 @@ public class RoleService : IRoleService public async Task> GetRoles(RoleFilter filter) { var db = _services.GetRequiredService(); + var roles = db.GetRoles(filter); + if (filter.IsInit() && roles.IsNullOrEmpty()) + { + await RefreshRoles(); + await Task.Delay(100); + roles = db.GetRoles(filter); + } + return roles; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs index d0c26be0..719dd805 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/HumanInterventionNeededFn.cs @@ -15,9 +15,9 @@ public class HumanInterventionNeededFn : IFunctionCallback public async Task Execute(RoleDialogModel message) { - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); + var hooks = _services + .GetRequiredService() + .HooksOrderByPriority; foreach (var hook in hooks) { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs index 646dd481..2ee6f9d7 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/ContinueExecuteTaskRoutingHandler.cs @@ -4,7 +4,7 @@ using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Planning; +using BotSharp.Core.Routing.Reasoning; namespace BotSharp.Core.Routing.Handlers; @@ -27,7 +27,7 @@ public class ContinueExecuteTaskRoutingHandler : RoutingHandlerBase//, IRoutingH public List Planers => new List { - nameof(HFPlanner) + nameof(HFReasoner) }; public ContinueExecuteTaskRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs index da58b98b..0d11f296 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/InterruptTaskExecutionRoutingHandler.cs @@ -1,7 +1,7 @@ using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Planning; +using BotSharp.Core.Routing.Reasoning; namespace BotSharp.Core.Routing.Handlers; @@ -19,7 +19,7 @@ public class InterruptTaskExecutionRoutingHandler : RoutingHandlerBase//, IRouti public List Planers => new List { - nameof(HFPlanner) + nameof(HFReasoner) }; public InterruptTaskExecutionRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs index 5b5b1025..038488eb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RetrieveDataFromAgentRoutingHandler.cs @@ -1,5 +1,5 @@ using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Planning; +using BotSharp.Core.Routing.Reasoning; namespace BotSharp.Core.Routing.Handlers; @@ -26,7 +26,7 @@ public class RetrieveDataFromAgentRoutingHandler : RoutingHandlerBase//, IRoutin public List Planers => new List { - nameof(HFPlanner) + nameof(HFReasoner) }; public RetrieveDataFromAgentRoutingHandler(IServiceProvider services, ILogger logger, RoutingSettings settings) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index a847f078..f83afedf 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -1,4 +1,3 @@ -using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; @@ -26,6 +25,14 @@ public class RoutingAgentHook : AgentHookBase var routing = _services.GetRequiredService(); var agents = routing.GetRoutableAgents(_agent.Profiles); + // 过滤 Planner + var planningRule = _agent.RoutingRules.FirstOrDefault(x => x.Type == "planner"); + if (planningRule != null) + { + var planners = agents.Where(x => x.Type == AgentType.Planning && x.Name != planningRule.Field).ToArray(); + agents = agents.Except(planners).ToArray(); + } + // Postprocess agent required fields, remove it if the states exists var states = _services.GetRequiredService(); foreach (var agent in agents) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlanParameter.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlanParameter.cs deleted file mode 100644 index 7bececc6..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/FirstStagePlanParameter.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -public class FirstStagePlanParameter -{ - [JsonPropertyName("input_args")] - public JsonDocument[] Parameters { get; set; } = new JsonDocument[0]; - - [JsonPropertyName("output_results")] - public string[] Results { get; set; } = new string[0]; - - public override string ToString() - { - return $"INPUTS:\r\n{JsonSerializer.Serialize(Parameters)}\r\n\r\nOUTPUTS:\r\n{JsonSerializer.Serialize(Results)}"; - } -} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/SecondStagePlan.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/SecondStagePlan.cs deleted file mode 100644 index f180c043..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/SecondStagePlan.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace BotSharp.Core.Routing.Planning; - -public class SecondStagePlan -{ - [JsonPropertyName("related_tables")] - public string[] Tables { get; set; } = new string[0]; - - [JsonPropertyName("description")] - public string Description { get; set; } = ""; - - [JsonPropertyName("tool_name")] - public string Tool { get; set; } = ""; - - [JsonPropertyName("input_args")] - public JsonDocument[] Parameters { get; set; } = new JsonDocument[0]; - - [JsonPropertyName("output_results")] - public string[] Results { get; set; } = new string[0]; -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/SecondStagePlanParameter.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/SecondStagePlanParameter.cs deleted file mode 100644 index 1d043740..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/TwoStagePlanner/SecondStagePlanParameter.cs +++ /dev/null @@ -1,4 +0,0 @@ -public class SecondStagePlanParameter : FirstStagePlanParameter -{ - -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/HFPlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs similarity index 71% rename from src/Infrastructure/BotSharp.Core/Routing/Planning/HFPlanner.cs rename to src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs index 8984da51..e7d9b49e 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/HFPlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/HFReasoner.cs @@ -1,17 +1,34 @@ -using BotSharp.Abstraction.Routing.Planning; +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + +using BotSharp.Abstraction.Routing.Reasoning; using BotSharp.Abstraction.Templating; -namespace BotSharp.Core.Routing.Planning; +namespace BotSharp.Core.Routing.Reasoning; /// -/// Human feedback based planner +/// Human feedback based reasoner /// -public class HFPlanner : IRoutingPlaner +public class HFReasoner : IRoutingReasoner { + public string Name => "Human-Feedback Reasoner"; private readonly IServiceProvider _services; private readonly ILogger _logger; - public HFPlanner(IServiceProvider services, ILogger logger) + public HFReasoner(IServiceProvider services, ILogger logger) { _services = services; _logger = logger; @@ -37,7 +54,7 @@ public class HFPlanner : IRoutingPlaner { new RoleDialogModel(AgentRole.User, next) { - FunctionName = nameof(HFPlanner), + FunctionName = nameof(HFReasoner), MessageId = messageId } }; @@ -60,7 +77,7 @@ public class HFPlanner : IRoutingPlaner } // Fix LLM malformed response - PlannerHelper.FixMalformedResponse(_services, inst); + ReasonerHelper.FixMalformedResponse(_services, inst); return inst; } @@ -87,13 +104,13 @@ public class HFPlanner : IRoutingPlaner public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) { var context = _services.GetRequiredService(); - context.Empty(reason: $"Agent queue is cleared by {nameof(HFPlanner)}"); + context.Empty(reason: $"Agent queue is cleared by {nameof(HFReasoner)}"); return true; } private string GetNextStepPrompt(Agent router) { - var template = router.Templates.First(x => x.Name == "planner_prompt.hf").Content; + var template = router.Templates.First(x => x.Name == "reasoner.hf").Content; var render = _services.GetRequiredService(); // update states var conv = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs similarity index 91% rename from src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs rename to src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs index a1d413f0..6bb23e97 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/InstructExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs @@ -1,6 +1,6 @@ -using BotSharp.Abstraction.Routing.Planning; +using BotSharp.Abstraction.Planning; -namespace BotSharp.Core.Routing.Planning; +namespace BotSharp.Core.Routing.Reasoning; public class InstructExecutor : IExecutor { diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs similarity index 73% rename from src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs rename to src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs index 6ebc5012..931b53e8 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/NaivePlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/NaiveReasoner.cs @@ -1,16 +1,37 @@ +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Routing.Planning; +using BotSharp.Abstraction.Routing.Reasoning; using BotSharp.Abstraction.Templating; -namespace BotSharp.Core.Routing.Planning; +namespace BotSharp.Core.Routing.Reasoning; -public class NaivePlanner : IRoutingPlaner +/// +/// simple or unsophisticated methods used to decide which specialized model or module in a system to engage for a given task. +/// +public class NaiveReasoner : IRoutingReasoner { private readonly IServiceProvider _services; private readonly ILogger _logger; - public NaivePlanner(IServiceProvider services, ILogger logger) + public string Name => "Naive Reasoner"; + + public NaiveReasoner(IServiceProvider services, ILogger logger) { _services = services; _logger = logger; @@ -46,7 +67,7 @@ public class NaivePlanner : IRoutingPlaner { new RoleDialogModel(AgentRole.User, next) { - FunctionName = nameof(NaivePlanner), + FunctionName = nameof(NaiveReasoner), MessageId = messageId } }; @@ -69,7 +90,7 @@ public class NaivePlanner : IRoutingPlaner } // Fix LLM malformed response - PlannerHelper.FixMalformedResponse(_services, inst); + ReasonerHelper.FixMalformedResponse(_services, inst); return inst; } @@ -99,7 +120,7 @@ public class NaivePlanner : IRoutingPlaner } else { - context.Empty(reason: $"Agent queue is cleared by {nameof(NaivePlanner)}"); + context.Empty(reason: $"Agent queue is cleared by {nameof(NaiveReasoner)}"); // context.Push(inst.OriginalAgent, "Push user goal agent"); } return true; @@ -107,7 +128,7 @@ public class NaivePlanner : IRoutingPlaner private string GetNextStepPrompt(Agent router) { - var template = router.Templates.First(x => x.Name == "planner_prompt.naive").Content; + var template = router.Templates.First(x => x.Name == "reasoner.naive").Content; var states = _services.GetRequiredService(); var render = _services.GetRequiredService(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs new file mode 100644 index 00000000..7e6d2624 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/OneStepForwardReasoner.cs @@ -0,0 +1,137 @@ +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + +using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.Routing.Models; +using BotSharp.Abstraction.Routing.Reasoning; +using BotSharp.Abstraction.Templating; + +namespace BotSharp.Core.Routing.Reasoning; + +/// +/// One-step forward reasoning is a straightforward reasoning approach where the model or agent evaluates its current state +/// and takes the next best logical step toward the solution without extensive lookahead or planning. +/// This type of reasoning involves making a decision based on the current situation and immediate context +/// rather than considering multiple future steps or possibilities. +/// +public class OneStepForwardReasoner : IRoutingReasoner +{ + public string Name => "One-Step-Forward-Reasoner"; + + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public OneStepForwardReasoner(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task GetNextInstruction(Agent router, string messageId, List dialogs) + { + 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); + + inst = 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++; + } + } + + // Fix LLM malformed response + ReasonerHelper.FixMalformedResponse(_services, inst); + + return inst; + } + + public async Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + // Set user content as Planner's question + message.FunctionName = inst.Function; + message.FunctionArgs = inst.Arguments == null ? "{}" : JsonSerializer.Serialize(inst.Arguments); + + return true; + } + + public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + var context = _services.GetRequiredService(); + if (inst.UnmatchedAgent) + { + var unmatchedAgentId = context.GetCurrentAgentId(); + + // Exclude the wrong routed agent + var agents = router.TemplateDict["routing_agents"] as RoutableAgent[]; + router.TemplateDict["routing_agents"] = agents.Where(x => x.AgentId != unmatchedAgentId).ToArray(); + + // Handover to Router; + context.Pop(); + } + else + { + context.Empty(reason: $"Agent queue is cleared by {nameof(OneStepForwardReasoner)}"); + // context.Push(inst.OriginalAgent, "Push user goal agent"); + } + return true; + } + + private string GetNextStepPrompt(Agent router) + { + var template = router.Templates.First(x => x.Name == "reasoner.one-step-forward").Content; + + var states = _services.GetRequiredService(); + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { StateConst.EXPECTED_ACTION_AGENT, states.GetState(StateConst.EXPECTED_ACTION_AGENT) }, + { StateConst.EXPECTED_GOAL_AGENT, states.GetState(StateConst.EXPECTED_GOAL_AGENT) } + }); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/PlannerHelper.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs similarity index 96% rename from src/Infrastructure/BotSharp.Core/Routing/Planning/PlannerHelper.cs rename to src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs index 4500ca1d..7c8debf6 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/PlannerHelper.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs @@ -1,6 +1,6 @@ -namespace BotSharp.Core.Routing.Planning; +namespace BotSharp.Core.Routing.Reasoning; -public static class PlannerHelper +public static class ReasonerHelper { /// /// Sometimes LLM hallucinates and fails to set function names correctly. diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 5c1f1903..b0527605 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -41,7 +41,8 @@ public class RoutingContext : IRoutingContext var agentService = _services.GetRequiredService(); _routerAgentIds = agentService.GetAgents(new AgentFilter { - Type = AgentType.Routing + Type = AgentType.Routing, + Pager = new Pagination { Size = 100 } }).Result.Items.Select(x => x.Id).ToArray(); } @@ -49,6 +50,17 @@ public class RoutingContext : IRoutingContext } } + /// + /// Entry agent + /// + public string EntryAgentId + { + get + { + return _stack.LastOrDefault() ?? string.Empty; + } + } + public bool IsEmpty => !_stack.Any(); public string GetCurrentAgentId() @@ -231,12 +243,16 @@ public class RoutingContext : IRoutingContext public Stack GetAgentStack() { - return new Stack(_stack); + var copy = _stack.ToList(); + copy.Reverse(); + return new Stack(copy); } public void SetAgentStack(Stack stack) { - _stack = new Stack(stack); + var copy = stack.ToList(); + copy.Reverse(); + _stack = new Stack(copy); } public void ResetAgentStack() diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs index 7a751057..c5317ba4 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingPlugin.cs @@ -1,10 +1,8 @@ -using BotSharp.Abstraction.Routing; -using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Routing.Planning; +using BotSharp.Abstraction.Routing.Reasoning; using BotSharp.Abstraction.Routing.Settings; using BotSharp.Abstraction.Settings; using BotSharp.Core.Routing.Hooks; -using BotSharp.Core.Routing.Planning; +using BotSharp.Core.Routing.Reasoning; using Microsoft.Extensions.Configuration; namespace BotSharp.Core.Routing; @@ -35,8 +33,9 @@ public class RoutingPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetPlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetPlanner.cs deleted file mode 100644 index 0fcaa5a1..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.GetPlanner.cs +++ /dev/null @@ -1,24 +0,0 @@ -using BotSharp.Abstraction.Routing.Enums; -using BotSharp.Abstraction.Routing.Planning; -using BotSharp.Core.Routing.Planning; - -namespace BotSharp.Core.Routing; - -public partial class RoutingService -{ - public IRoutingPlaner GetPlanner(Agent router) - { - var rule = router.RoutingRules.FirstOrDefault(x => x.Type == RuleType.Planner); - - var planner = _services.GetServices(). - FirstOrDefault(x => x.GetType().Name.EndsWith(rule.Field)); - - if (planner == null) - { - _logger.LogError($"Can't find specific planner named {rule.Field}"); - return _services.GetRequiredService(); - } - - return planner; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs index 3b1f0566..d88b65ba 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs @@ -1,6 +1,5 @@ using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Routing.Models; -using System.Drawing; namespace BotSharp.Core.Routing; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs new file mode 100644 index 00000000..b7dc6cc3 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InstructLoop.cs @@ -0,0 +1,116 @@ +using BotSharp.Abstraction.Infrastructures.Enums; +using BotSharp.Abstraction.Planning; +using BotSharp.Abstraction.Routing.Enums; +using BotSharp.Abstraction.Routing.Reasoning; + +namespace BotSharp.Core.Routing; + +public partial class RoutingService +{ + public async Task InstructLoop(RoleDialogModel message, List dialogs) + { + RoleDialogModel response = default; + + var agentService = _services.GetRequiredService(); + var convService = _services.GetRequiredService(); + var storage = _services.GetRequiredService(); + + _router = await agentService.LoadAgent(message.CurrentAgentId); + + var states = _services.GetRequiredService(); + var executor = _services.GetRequiredService(); + + var reasoner = GetReasoner(_router); + + _context.Push(_router.Id); + + // Handle multi-language for input + var agentSettings = _services.GetRequiredService(); + if (agentSettings.EnableTranslator) + { + var translator = _services.GetRequiredService(); + + var language = states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH); + if (language != LanguageType.ENGLISH) + { + message.SecondaryContent = message.Content; + message.Content = await translator.Translate(_router, message.MessageId, message.Content, + language: LanguageType.ENGLISH, + clone: false); + } + } + + dialogs.Add(message); + storage.Append(convService.ConversationId, message); + + // Get first instruction + _router.TemplateDict["conversation"] = await GetConversationContent(dialogs); + var inst = await reasoner.GetNextInstruction(_router, message.MessageId, dialogs); + + int loopCount = 1; + while (true) + { + await HookEmitter.Emit(_services, async hook => + await hook.OnRoutingInstructionReceived(inst, message) + ); + + // Save states + states.SaveStateByArgs(inst.Arguments); + +#if DEBUG + Console.WriteLine($"*** Next Instruction *** {inst}"); +#else + _logger.LogInformation($"*** Next Instruction *** {inst}"); +#endif + await reasoner.AgentExecuting(_router, inst, message, dialogs); + + // Handover to Task Agent + if (inst.HandleDialogsByPlanner) + { + var dialogWithoutContext = reasoner.BeforeHandleContext(inst, message, dialogs); + response = await executor.Execute(this, inst, message, dialogWithoutContext); + reasoner.AfterHandleContext(dialogs, dialogWithoutContext); + } + else + { + response = await executor.Execute(this, inst, message, dialogs); + } + + await reasoner.AgentExecuted(_router, inst, response, dialogs); + + if (loopCount >= reasoner.MaxLoopCount || _context.IsEmpty) + { + break; + } + + // Get next instruction from Planner + _router.TemplateDict["conversation"] = await GetConversationContent(dialogs); + inst = await reasoner.GetNextInstruction(_router, message.MessageId, dialogs); + loopCount++; + } + + return response; + } + + public IRoutingReasoner GetReasoner(Agent router) + { + var rule = router.RoutingRules.FirstOrDefault(x => x.Type == RuleType.Reasoner); + + if (rule == null) + { + _logger.LogError($"Can't find any reasoner"); + return _services.GetServices().First(x => x.Name == "Naive Reasoner"); + } + + var reasoner = _services.GetServices().FirstOrDefault(x => x.Name == rule.Field); + + if (reasoner == null) + { + _logger.LogError($"Can't find specific reasoner named {rule.Field}"); + // Default use NaiveReasoner + return _services.GetServices().First(x => x.Name == "Naive Reasoner"); + } + + return reasoner; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index 25ab3552..39c8ed23 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -4,13 +4,11 @@ namespace BotSharp.Core.Routing; public partial class RoutingService { - //private int _currentRecursionDepth = 0; public async Task InvokeAgent(string agentId, List dialogs) { var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); - //_currentRecursionDepth++; Context.IncreaseRecursiveCounter(); if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) { diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 64b96136..fb074dfc 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -18,9 +18,9 @@ public partial class RoutingService var clonedMessage = RoleDialogModel.From(message); clonedMessage.FunctionName = name; - var hooks = _services.GetServices() - .OrderBy(x => x.Priority) - .ToList(); + var hooks = _services + .GetRequiredService() + .HooksOrderByPriority; var progressService = _services.GetService(); diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index 770acaf9..ef32f018 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -1,6 +1,4 @@ -using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Routing.Planning; using BotSharp.Abstraction.Routing.Settings; namespace BotSharp.Core.Routing; @@ -16,21 +14,6 @@ public partial class RoutingService : IRoutingService public IRoutingContext Context => _context; public Agent Router => _router; - //public int GetRecursiveCounter() - //{ - // return _currentRecursionDepth; - //} - - //public void SetRecursiveCounter(int counter) - //{ - // _currentRecursionDepth = counter; - //} - - //public void ResetRecursiveCounter() - //{ - // _currentRecursionDepth = 0; - //} - public RoutingService( IServiceProvider services, RoutingSettings settings, @@ -75,97 +58,12 @@ public partial class RoutingService : IRoutingService return response; } - public async Task InstructLoop(RoleDialogModel message, List dialogs) - { - RoleDialogModel response = default; - - var agentService = _services.GetRequiredService(); - var convService = _services.GetRequiredService(); - var storage = _services.GetRequiredService(); - - _router = await agentService.LoadAgent(message.CurrentAgentId); - - var states = _services.GetRequiredService(); - var executor = _services.GetRequiredService(); - - var planner = GetPlanner(_router); - - _context.Push(_router.Id); - - // Handle multi-language for input - var agentSettings = _services.GetRequiredService(); - if (agentSettings.EnableTranslator) - { - var translator = _services.GetRequiredService(); - - var language = states.GetState(StateConst.LANGUAGE, LanguageType.ENGLISH); - if (language != LanguageType.ENGLISH) - { - message.SecondaryContent = message.Content; - message.Content = await translator.Translate(_router, message.MessageId, message.Content, - language: LanguageType.ENGLISH, - clone: false); - } - } - - dialogs.Add(message); - storage.Append(convService.ConversationId, message); - - // Get first instruction - _router.TemplateDict["conversation"] = await GetConversationContent(dialogs); - var inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs); - - int loopCount = 1; - while (true) - { - await HookEmitter.Emit(_services, async hook => - await hook.OnRoutingInstructionReceived(inst, message) - ); - - // Save states - states.SaveStateByArgs(inst.Arguments); - -#if DEBUG - Console.WriteLine($"*** Next Instruction *** {inst}"); -#else - _logger.LogInformation($"*** Next Instruction *** {inst}"); -#endif - await planner.AgentExecuting(_router, inst, message, dialogs); - - // Handover to Task Agent - if (inst.HandleDialogsByPlanner) - { - var dialogWithoutContext = planner.BeforeHandleContext(inst, message, dialogs); - response = await executor.Execute(this, inst, message, dialogWithoutContext); - planner.AfterHandleContext(dialogs, dialogWithoutContext); - } - else - { - response = await executor.Execute(this, inst, message, dialogs); - } - - await planner.AgentExecuted(_router, inst, response, dialogs); - - if (loopCount >= planner.MaxLoopCount || _context.IsEmpty) - { - break; - } - - // Get next instruction from Planner - _router.TemplateDict["conversation"] = await GetConversationContent(dialogs); - inst = await planner.GetNextInstruction(_router, message.MessageId, dialogs); - loopCount++; - } - - return response; - } - public List GetHandlers(Agent router) { - var planer = GetPlanner(router); + var reasoner = GetReasoner(router); return _services.GetServices() - .Where(x => x.Planers == null || x.Planers.Contains(planer.GetType().Name)) + .Where(x => x.Planers == null || x.Planers.Contains(reasoner.GetType().Name)) .Where(x => !string.IsNullOrEmpty(x.Description)) .Select((x, i) => new RoutingHandlerDef { @@ -184,11 +82,10 @@ public partial class RoutingService : IRoutingService var filter = new AgentFilter { - Disabled = false, - Type = AgentType.Task + Disabled = false }; var agents = db.GetAgents(filter); - var records = agents.SelectMany(x => + var records = agents.Where(x => x.Type == AgentType.Task || x.Type == AgentType.Planning).SelectMany(x => { x.RoutingRules.ForEach(r => { @@ -210,16 +107,16 @@ public partial class RoutingService : IRoutingService var filter = new AgentFilter { - Disabled = false, - Type = AgentType.Task + Disabled = false }; var agents = db.GetAgents(filter); - var routableAgents = agents.Select(x => new RoutableAgent + var routableAgents = agents.Where(x => x.Type == AgentType.Task || x.Type == AgentType.Planning).Select(x => new RoutableAgent { AgentId = x.Id, Description = x.Description, Name = x.Name, + Type = x.Type, Profiles = x.Profiles, RequiredFields = x.RoutingRules .Where(p => p.Required) diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index 33b27177..c16a8c4b 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -1,10 +1,10 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Models; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; using BotSharp.Abstraction.Translation.Models; using Fluid; +using System.Collections; +using System.Reflection; namespace BotSharp.Core.Templating; @@ -48,4 +48,47 @@ public class TemplateRender : ITemplateRender return template; } } + + + public void Register(Type type) + { + if (type == null || IsStringType(type)) return; + + if (IsListType(type)) + { + if (type.IsGenericType) + { + var genericType = type.GetGenericArguments()[0]; + Register(genericType); + } + } + else if (IsTrackToNextLevel(type)) + { + _options.MemberAccessStrategy.Register(type); + var props = type.GetProperties(); + foreach (var prop in props) + { + Register(prop.PropertyType); + } + } + } + + + #region Private methods + private static bool IsStringType(Type type) + { + return type == typeof(string); + } + + private static bool IsListType(Type type) + { + var interfaces = type.GetTypeInfo().ImplementedInterfaces; + return type.IsArray || interfaces.Any(x => x.Name == typeof(IEnumerable).Name); + } + + private static bool IsTrackToNextLevel(Type type) + { + return type.IsClass || type.IsInterface || type.IsAbstract; + } + #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs index 453b2277..7b81ac10 100644 --- a/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs +++ b/src/Infrastructure/BotSharp.Core/Translation/TranslationService.cs @@ -1,3 +1,19 @@ +/***************************************************************************** + Copyright 2024 Written by Jicheng Lu. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + using BotSharp.Abstraction.Infrastructures.Enums; using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Options; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 9e74d961..9bc436a7 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -148,7 +148,7 @@ public class UserService : IUserService public async Task GetAffiliateToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (id, password) = base64.SplitAsTuple(":"); + var (id, password, regionCode) = base64.SplitAsTuple(":"); var db = _services.GetRequiredService(); var record = db.GetAffiliateUserByPhone(id); var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Affiliate; @@ -170,9 +170,9 @@ public class UserService : IUserService public async Task GetAdminToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (id, password) = base64.SplitAsTuple(":"); + var (id, password, regionCode) = base64.SplitAsTuple(":"); var db = _services.GetRequiredService(); - var record = db.GetUserByPhone(id,"admin"); + var record = db.GetUserByPhone(id, type: UserType.Internal); var isCanLogin = record != null && !record.IsDisabled && record.Type == UserType.Internal && new List { @@ -210,13 +210,13 @@ public class UserService : IUserService public async Task GetToken(string authorization) { var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization)); - var (id, password) = base64.SplitAsTuple(":"); + var (id, password, regionCode) = base64.SplitAsTuple(":"); var db = _services.GetRequiredService(); var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id); if (record == null) { - record = db.GetUserByPhone(id); + record = db.GetUserByPhone(id, regionCode: regionCode); } if (record != null && record.Type == UserType.Affiliate) @@ -525,7 +525,7 @@ public class UserService : IUserService public async Task VerifyEmailExisting(string email) { - if (string.IsNullOrEmpty(email)) + if (string.IsNullOrWhiteSpace(email)) { return true; } @@ -542,13 +542,13 @@ public class UserService : IUserService public async Task VerifyPhoneExisting(string phone, string regionCode) { - if (string.IsNullOrEmpty(phone)) + if (string.IsNullOrWhiteSpace(phone)) { return true; } var db = _services.GetRequiredService(); - var UserByphone = db.GetUserByPhone(phone, regionCode); + var UserByphone = db.GetUserByPhone(phone, regionCode: regionCode); if (UserByphone != null && UserByphone.Verified) { return true; @@ -570,7 +570,7 @@ public class UserService : IUserService if (!string.IsNullOrEmpty(user.Phone)) { - record = db.GetUserByPhone(user.Phone); + record = db.GetUserByPhone(user.Phone, regionCode: user.RegionCode); } if (!string.IsNullOrEmpty(user.Email)) @@ -736,4 +736,42 @@ public class UserService : IUserService } return true; } + + public async Task AddDashboardConversation(string userId, string conversationId) + { + var db = _services.GetRequiredService(); + db.AddDashboardConversation(userId, conversationId); + + await Task.CompletedTask; + return true; + } + + public async Task RemoveDashboardConversation(string userId, string conversationId) + { + var db = _services.GetRequiredService(); + db.RemoveDashboardConversation(userId, conversationId); + + await Task.CompletedTask; + return true; + } + + public async Task UpdateDashboardConversation(string userId, DashboardConversation newDashConv) + { + var db = _services.GetRequiredService(); + var dashConv = db.GetDashboard(userId)?.ConversationList.FirstOrDefault(x => string.Equals(x.ConversationId, newDashConv.ConversationId)); + if (dashConv == null) return; + dashConv.Name = newDashConv.Name ?? dashConv.Name; + dashConv.Instruction = newDashConv.Instruction ?? dashConv.Instruction; + db.UpdateDashboardConversation(userId, dashConv); + await Task.CompletedTask; + return; + } + + public async Task GetDashboard(string userId) + { + var db = _services.GetRequiredService(); + var dash = db.GetDashboard(); + await Task.CompletedTask; + return dash; + } } diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index f68cba9b..4a74f1d0 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -14,7 +14,7 @@ global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.Plugins; global using BotSharp.Abstraction.Agents; global using BotSharp.Abstraction.Conversations; -global using BotSharp.Abstraction.Knowledges; +global using BotSharp.Abstraction.Crontab.Models; global using BotSharp.Abstraction.Users; global using BotSharp.Abstraction.Roles; global using BotSharp.Abstraction.Roles.Models; diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/agent.json b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/agent.json index 3159bb08..5e4186dc 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/agent.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/agent.json @@ -8,11 +8,15 @@ "iconUrl": "https://cdn.iconscout.com/icon/premium/png-256-thumb/route-1613278-1368497.png", "disabled": false, "isPublic": true, - "profiles": [ "tool" ], + "profiles": [ "planning", "fallback", "human" ], "routingRules": [ + { + "type": "reasoner", + "field": "One-Step-Forward-Reasoner" + }, { "type": "planner", - "field": "HFPlanner" + "field": "SQL-Planner" } ] } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.hf.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.hf.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.hf.liquid rename to src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.hf.liquid diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.naive.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.naive.liquid rename to src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.naive.liquid diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.one-step-forward.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.one-step-forward.liquid new file mode 100644 index 00000000..a492175b --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.one-step-forward.liquid @@ -0,0 +1,3 @@ +Analyze the user's problem. Which prerequisite task needs to be completed? Output the next step of routing instructions. +Check the job responsibilities of the routable Agent and do not transfer to an Agent that exceeds the scope of responsibility. +If the user request may require other tools or services, route to the planner agent. diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.get_remaining_task.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.sequential.get_remaining_task.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.get_remaining_task.liquid rename to src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.sequential.get_remaining_task.liquid diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.sequential.liquid similarity index 100% rename from src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/planner_prompt.sequential.liquid rename to src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/reasoner.sequential.liquid diff --git a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs index 1cb09f3e..a9a9478f 100644 --- a/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs +++ b/src/Infrastructure/BotSharp.Logger/Hooks/RateLimitConversationHook.cs @@ -34,7 +34,7 @@ public class RateLimitConversationHook : ConversationHookBase } // Check message sending frequency - var userSents = _dialogs.Where(x => x.Role == AgentRole.User) + var userSents = Dialogs.Where(x => x.Role == AgentRole.User) .TakeLast(2).ToList(); if (userSents.Count > 1) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index 5ce68c2a..d8b701c9 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -1,4 +1,4 @@ -using BotSharp.Abstraction.Users.Enums; +using BotSharp.Abstraction.Agents.Models; namespace BotSharp.OpenAPI.Controllers; @@ -59,11 +59,7 @@ public class AgentController : ControllerBase var userService = _services.GetRequiredService(); var auth = await userService.GetUserAuthorizations(new List { targetAgent.Id }); - - targetAgent.Editable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Edit); - targetAgent.Chatable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Chat); - targetAgent.Trainable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Train); - targetAgent.Evaluable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Evaluate); + targetAgent.Actions = auth.GetAllowedAgentActions(targetAgent.Id); return targetAgent; } @@ -90,10 +86,7 @@ public class AgentController : ControllerBase agents = pagedAgents?.Items?.Select(x => { var model = AgentViewModel.FromAgent(x); - model.Editable = auth.IsAgentActionAllowed(x.Id, UserAction.Edit); - model.Chatable = auth.IsAgentActionAllowed(x.Id, UserAction.Chat); - model.Trainable = auth.IsAgentActionAllowed(x.Id, UserAction.Train); - model.Evaluable = auth.IsAgentActionAllowed(x.Id, UserAction.Evaluate); + model.Actions = auth.GetAllowedAgentActions(x.Id); return model; })?.ToList() ?? []; @@ -153,9 +146,9 @@ public class AgentController : ControllerBase return await _agentService.DeleteAgent(agentId); } - [HttpGet("/agent/utilities")] - public IEnumerable GetAgentUtilities() + [HttpGet("/agent/utility/options")] + public IEnumerable GetAgentUtilityOptions() { - return _agentService.GetAgentUtilities(); + return _agentService.GetAgentUtilityOptions(); } } \ 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 2da3251b..266c5ac5 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -223,6 +223,30 @@ public class ConversationController : ControllerBase return response != null; } + [HttpPut("/conversation/{conversationId}/update-title-alias")] + public async Task UpdateConversationTitleAlias([FromRoute] string conversationId, [FromBody] UpdateConversationTitleAliasModel newTile) + { + var userService = _services.GetRequiredService(); + var conversationService = _services.GetRequiredService(); + + var user = await userService.GetUser(_user.Id); + var filter = new ConversationFilter + { + Id = conversationId, + UserId = user.Role != UserRole.Admin ? user.Id : null + }; + var conversations = await conversationService.GetConversations(filter); + + if (conversations.Items.IsNullOrEmpty()) + { + return false; + } + + var response = await conversationService.UpdateConversationTitleAlias(conversationId, newTile.NewTitleAlias); + return response != null; + } + + [HttpPut("/conversation/{conversationId}/update-tags")] public async Task UpdateConversationTags([FromRoute] string conversationId, [FromBody] UpdateConversationRequest request) { @@ -511,6 +535,28 @@ public class ConversationController : ControllerBase } #endregion + #region miscellaneous + [HttpPut("/agent/{agentId}/conversation/{conversationId}/dashboard")] + public async Task PinConversationToDashboard([FromRoute] string agentId, [FromRoute] string conversationId) + { + var userService = _services.GetRequiredService(); + + var user = await userService.GetUser(_user.Id); + var pinned = await userService.AddDashboardConversation(user.Id, conversationId); + return pinned; + } + + [HttpDelete("/agent/{agentId}/conversation/{conversationId}/dashboard")] + public async Task UnpinConversationFromDashboard([FromRoute] string agentId, [FromRoute] string conversationId) + { + var userService = _services.GetRequiredService(); + + var user = await userService.GetUser(_user.Id); + var unpinned = await userService.RemoveDashboardConversation(user.Id, conversationId); + return unpinned; + } + #endregion + #region Private methods private void SetStates(IConversationService conv, NewMessageModel input) { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs new file mode 100644 index 00000000..f75467fe --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/DashboardController.cs @@ -0,0 +1,73 @@ +using BotSharp.Abstraction.Options; +using BotSharp.Abstraction.Users.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.OpenAPI.Controllers; + +[Authorize] +[ApiController] +public class DashboardController : ControllerBase +{ + private readonly IServiceProvider _services; + private readonly IUserIdentity _user; + + public DashboardController(IServiceProvider services, + IUserIdentity user, + BotSharpOptions options) + { + _services = services; + _user = user; + + } + #region User Components + [HttpGet("/dashboard/components")] + public async Task GetComponents(string userId) + { + var userService = _services.GetRequiredService(); + var dashboardProfile = await userService.GetDashboard(userId); + if (dashboardProfile == null) return new UserDashboardModel(); + var result = new UserDashboardModel + { + ConversationList = dashboardProfile.ConversationList.Select( + x => new UserDashboardConversationModel + { + Name = x.Name, + ConversationId = x.ConversationId, + Instruction = x.Instruction + } + ).ToList() + }; + return result; + } + + [HttpPost("/dashboard/component/conversation")] + public async Task UpdateDashboardConversationInstruction(string userId, UserDashboardConversationModel dashConv) + { + if (string.IsNullOrEmpty(dashConv.Name) && string.IsNullOrEmpty(dashConv.Instruction)) + { + return; + } + var newDashConv = new DashboardConversation + { + Id = Guid.Empty.ToString(), + ConversationId = dashConv.ConversationId + }; + if (!string.IsNullOrEmpty(dashConv.Name)) + { + newDashConv.Name = dashConv.Name; + } + if (!string.IsNullOrEmpty(dashConv.Instruction)) + { + newDashConv.Instruction = dashConv.Instruction; + } + + var userService = _services.GetRequiredService(); + await userService.UpdateDashboardConversation(userId, newDashConv); + return; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs index 78f75833..44c257df 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentCreationModel.cs @@ -48,7 +48,12 @@ public class AgentCreationModel /// Combine different Agents together to form a Profile. /// public List Profiles { get; set; } = new(); - public List Utilities { get; set; } = new(); + + public bool MergeUtility { get; set; } + + public int? MaxMessageCount { get; set; } + + public List Utilities { get; set; } = new(); public List RoutingRules { get; set; } = new(); public AgentLlmConfig? LlmConfig { get; set; } @@ -68,6 +73,8 @@ public class AgentCreationModel IsPublic = IsPublic, Type = Type, Disabled = Disabled, + MergeUtility = MergeUtility, + MaxMessageCount = MaxMessageCount, Profiles = Profiles, RoutingRules = RoutingRules?.Select(x => RoutingRuleUpdateModel.ToDomainElement(x))?.ToList() ?? new List(), LlmConfig = LlmConfig diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs index d64530c6..141f0662 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentUpdateModel.cs @@ -31,10 +31,13 @@ public class AgentUpdateModel /// public List? Samples { get; set; } + [JsonPropertyName("merge_utility")] + public bool MergeUtility { get; set; } + /// /// Utilities /// - public List? Utilities { get; set; } + public List? Utilities { get; set; } /// /// Functions @@ -54,6 +57,9 @@ public class AgentUpdateModel public bool Disabled { get; set; } + [JsonPropertyName("max_message_count")] + public int? MaxMessageCount { get; set; } + /// /// Profile by channel /// @@ -73,6 +79,8 @@ public class AgentUpdateModel Description = Description ?? string.Empty, IsPublic = IsPublic, Disabled = Disabled, + MergeUtility = MergeUtility, + MaxMessageCount = MaxMessageCount, Type = Type, Profiles = Profiles ?? new List(), RoutingRules = RoutingRules?.Select(x => RoutingRuleUpdateModel.ToDomainElement(x))?.ToList() ?? new List(), @@ -81,7 +89,7 @@ public class AgentUpdateModel Templates = Templates ?? new List(), Functions = Functions ?? new List(), Responses = Responses ?? new List(), - Utilities = Utilities ?? new List(), + Utilities = Utilities ?? new List(), LlmConfig = LlmConfig }; diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs index ca814177..36a8900f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs @@ -20,7 +20,10 @@ public class AgentViewModel public List Functions { get; set; } public List Responses { get; set; } public List Samples { get; set; } - public List Utilities { get; set; } + + [JsonPropertyName("merge_utility")] + public bool MergeUtility { get; set; } + public List Utilities { get; set; } [JsonPropertyName("is_public")] public bool IsPublic { get; set; } @@ -33,8 +36,7 @@ public class AgentViewModel [JsonPropertyName("icon_url")] public string IconUrl { get; set; } - public List Profiles { get; set; } - = new List(); + public List Profiles { get; set; } = new(); [JsonPropertyName("routing_rules")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -44,12 +46,13 @@ public class AgentViewModel [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AgentLlmConfig? LlmConfig { get; set; } + [JsonPropertyName("max_message_count")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxMessageCount { get; set; } + public PluginDef Plugin { get; set; } - public bool Editable { get; set; } - public bool Chatable { get; set; } - public bool Trainable { get; set; } - public bool Evaluable { get; set; } + public IEnumerable? Actions { get; set; } [JsonPropertyName("created_datetime")] public DateTime CreatedDateTime { get; set; } @@ -74,7 +77,9 @@ public class AgentViewModel Utilities = agent.Utilities, IsPublic= agent.IsPublic, Disabled = agent.Disabled, + MergeUtility = agent.MergeUtility, IconUrl = agent.IconUrl, + MaxMessageCount = agent.MaxMessageCount, Profiles = agent.Profiles ?? new List(), RoutingRules = agent.RoutingRules, LlmConfig = agent.LlmConfig, diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs index 05f8cb89..a660a525 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationViewModel.cs @@ -12,8 +12,12 @@ public class ConversationViewModel [JsonPropertyName("agent_name")] public string AgentName { get; set; } + [JsonPropertyName("title")] public string Title { get; set; } = string.Empty; + [JsonPropertyName("title_alias")] + public string TitleAlias { get; set; } = string.Empty; + public UserViewModel User { get; set; } = new UserViewModel(); public string Event { get; set; } @@ -47,6 +51,7 @@ public class ConversationViewModel }, AgentId = sess.AgentId, Title = sess.Title, + TitleAlias = sess.TitleAlias, Channel = sess.Channel, Status = sess.Status, TaskId = sess.TaskId, diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs new file mode 100644 index 00000000..5d9ac2fd --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/UpdateConversationTitleAliasModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace BotSharp.OpenAPI.ViewModels.Conversations; + +public class UpdateConversationTitleAliasModel +{ + [Required] + public string NewTitleAlias { get; set; } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs index c55da5fa..b529a832 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserCreationModel.cs @@ -13,6 +13,7 @@ public class UserCreationModel public string Type { get; set; } = UserType.Client; public string Role { get; set; } = UserRole.User; public string RegionCode { get; set; } = "CN"; + public string? ReferralCode { get; set; } public User ToUser() { return new User @@ -25,7 +26,8 @@ public class UserCreationModel Password = Password, Role = Role, Type = Type, - RegionCode = RegionCode + RegionCode = RegionCode, + ReferralCode = ReferralCode }; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs new file mode 100644 index 00000000..3a5f3491 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserDashboardConversationModel.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace BotSharp.OpenAPI.ViewModels.Users; +public class UserDashboardModel +{ + + [JsonPropertyName("conversation_list")] + public IList ConversationList { get; set; } = []; +} + +public class UserDashboardConversationModel +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("conversation_id")] + public string? ConversationId { get; set; } + + [JsonPropertyName("instruction")] + public string? Instruction { get; set; } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index fae5873b..b3183bc9 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -57,6 +57,8 @@ public class UserViewModel UserName = user.UserName, FirstName = user.FirstName, LastName = user.LastName, + //Email = Utilities.HideMiddleDigits(user.Email, true), + //Phone = Utilities.HideMiddleDigits((!string.IsNullOrWhiteSpace(user.Phone) ? user.Phone.Replace("+86", String.Empty) : user.Phone)), Email = user.Email, Phone = !string.IsNullOrWhiteSpace(user.Phone) ? user.Phone.Replace("+86", String.Empty) : user.Phone, Type = user.Type, diff --git a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj index c5bd88d0..d053f4d6 100644 --- a/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj +++ b/src/Plugins/BotSharp.Plugin.AnthropicAI/BotSharp.Plugin.AnthropicAI.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/AudioHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/AudioHandlerPlugin.cs index 2c289907..d79ca6c2 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/AudioHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/AudioHandlerPlugin.cs @@ -17,7 +17,6 @@ public class AudioHandlerPlugin : IBotSharpPlugin }); services.AddScoped(); - services.AddScoped(); services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/BotSharp.Plugin.AudioHandler.csproj b/src/Plugins/BotSharp.Plugin.AudioHandler/BotSharp.Plugin.AudioHandler.csproj index 4ae8c7d2..ace72c5c 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/BotSharp.Plugin.AudioHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/BotSharp.Plugin.AudioHandler.csproj @@ -24,15 +24,15 @@ - - + + - + PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/Enums/UtilityName.cs index d11bd65a..51c86863 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/Enums/UtilityName.cs @@ -2,5 +2,5 @@ namespace BotSharp.Plugin.AudioHandler.Enums; public class UtilityName { - public const string AudioHandler = "audio-handler"; + public const string AudioHandler = "audio.audio-handler"; } diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/Functions/HandleAudioRequestFn.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/Functions/HandleAudioRequestFn.cs index 34ec530c..9c0ab4f4 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/Functions/HandleAudioRequestFn.cs +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/Functions/HandleAudioRequestFn.cs @@ -5,7 +5,7 @@ namespace BotSharp.Plugin.AudioHandler.Functions; public class HandleAudioRequestFn : IFunctionCallback { - public string Name => "handle_audio_request"; + public string Name => "util-audio-handle_audio_request"; public string Indication => "Handling audio request"; private readonly IServiceProvider _serviceProvider; diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerHook.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerHook.cs deleted file mode 100644 index 80acb149..00000000 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerHook.cs +++ /dev/null @@ -1,60 +0,0 @@ -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; - -namespace BotSharp.Plugin.AudioHandler.Hooks; - -public class AudioHandlerHook : AgentHookBase, IAgentHook -{ - private const string HANDLER_AUDIO = "handle_audio_request"; - - public override string SelfId => string.Empty; - - public AudioHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.AudioHandler); - - if (isEnabled && isConvMode) - { - AddUtility(agent, HANDLER_AUDIO); - } - - base.OnAgentLoaded(agent); - } - - private void AddUtility(Agent agent, string functionName) - { - var (prompt, fn) = GetPromptAndFunction(functionName); - - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - private (string, FunctionDef?) GetPromptAndFunction(string functionName) - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName)); - return (prompt, loadAttachmentFn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerUtilityHook.cs index ac3f0ed7..4cb6d61c 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/Hooks/AudioHandlerUtilityHook.cs @@ -2,8 +2,18 @@ namespace BotSharp.Plugin.AudioHandler.Hooks; public class AudioHandlerUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private const string PREFIX = "util-audio-"; + private const string HANDLER_AUDIO = $"{PREFIX}handle_audio_request"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.AudioHandler); + var utility = new AgentUtility + { + Name = UtilityName.AudioHandler, + Functions = [new(HANDLER_AUDIO)], + Templates = [new($"{HANDLER_AUDIO}.fn")] + }; + + utilities.Add(utility); } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_audio_request.json b/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-audio-handle_audio_request.json similarity index 94% rename from src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_audio_request.json rename to src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-audio-handle_audio_request.json index c0603b1a..7655f691 100644 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_audio_request.json +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-audio-handle_audio_request.json @@ -1,5 +1,5 @@ { - "name": "handle_audio_request", + "name": "util-audio-handle_audio_request", "description": "If the user requests to transcribe or summarize audio content, you need to call this function to transcribe the audio content to raw texts or provide sunmmary based on raw texts transcribed from audio", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_audio_request.fn.liquid b/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_audio_request.fn.liquid deleted file mode 100644 index 95f17ae1..00000000 --- a/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_audio_request.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Please call handle_audio_request if user wants to transcribe or summarize the content of a audio file. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-audio-handle_audio_request.fn.liquid b/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-audio-handle_audio_request.fn.liquid new file mode 100644 index 00000000..b49c3440 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AudioHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-audio-handle_audio_request.fn.liquid @@ -0,0 +1 @@ +Please call util-audio-handle_audio_request if user wants to transcribe or summarize the content of a audio file. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj index 4f076a9e..4268cf38 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj +++ b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs b/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs index 5bd9a8ce..e7cf082f 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/ChatHubPlugin.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Loggers; using BotSharp.Abstraction.Routing; +using BotSharp.Core.Crontab.Abstraction; using BotSharp.Plugin.ChatHub.Hooks; using Microsoft.Extensions.Configuration; @@ -21,7 +22,9 @@ public class ChatHubPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index b282fa53..6b425b4b 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -29,6 +29,7 @@ public class ChatHubConversationHook : ConversationHookBase _chatHub = chatHub; _user = user; _options = options; + Priority = -1; // Make sure this hook is the top one. } public override async Task OnConversationInitialized(Conversation conversation) diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs new file mode 100644 index 00000000..4f7ef3d2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubCrontabHook.cs @@ -0,0 +1,46 @@ +using BotSharp.Abstraction.Crontab.Models; +using BotSharp.Core.Crontab.Abstraction; +using Microsoft.AspNetCore.SignalR; + +namespace BotSharp.Plugin.ChatHub.Hooks; + +public class ChatHubCrontabHook : ICrontabHook +{ + private readonly IServiceProvider _services; + private readonly IHubContext _chatHub; + private readonly IUserIdentity _user; + private readonly IConversationStorage _storage; + private readonly BotSharpOptions _options; + + public ChatHubCrontabHook(IServiceProvider services, + IHubContext chatHub, + IUserIdentity user, + IConversationStorage storage, + BotSharpOptions options) + { + _services = services; + _chatHub = chatHub; + _user = user; + _storage = storage; + _options = options; + } + + public async Task OnCronTriggered(CrontabItem item) + { + var json = JsonSerializer.Serialize(new ChatResponseModel() + { + ConversationId = item.ConversationId, + MessageId = Guid.NewGuid().ToString(), + Text = item.ExecutionResult, + Function = "", + Sender = new UserViewModel() + { + FirstName = "Crontab", + LastName = "AI", + Role = AgentRole.Assistant + } + }, _options.JsonSerializerOptions); + + await _chatHub.Clients.User(item.UserId).SendAsync("OnNotificationGenerated", json); + } +} diff --git a/src/Plugins/BotSharp.Plugin.CodeDriver/data/agents/c0ded7d9-3f9d-4ef6-b7ce-56a892dcef62/agent.json b/src/Plugins/BotSharp.Plugin.CodeDriver/data/agents/c0ded7d9-3f9d-4ef6-b7ce-56a892dcef62/agent.json index 4ed8765c..038c969e 100644 --- a/src/Plugins/BotSharp.Plugin.CodeDriver/data/agents/c0ded7d9-3f9d-4ef6-b7ce-56a892dcef62/agent.json +++ b/src/Plugins/BotSharp.Plugin.CodeDriver/data/agents/c0ded7d9-3f9d-4ef6-b7ce-56a892dcef62/agent.json @@ -8,7 +8,7 @@ "updatedDateTime": "2024-11-23T00:00:00Z", "disabled": false, "isPublic": true, - "profiles": [ "database" ], + "profiles": [ "coding" ], "llmConfig": { "provider": "openai", "model": "gpt-4o", diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj b/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj index 3aa65e97..f1a6c28e 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/BotSharp.Plugin.EmailHandler.csproj @@ -11,24 +11,23 @@ - - - - - + + + + - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/EmailHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/EmailHandlerPlugin.cs index 5d7ff586..fffc8c31 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/EmailHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/EmailHandlerPlugin.cs @@ -20,8 +20,6 @@ namespace BotSharp.Plugin.EmailHandler return settingService.Bind("EmailSender"); }); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); var emailReaderSettings = new EmailReaderSettings(); diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Enums/UtilityName.cs index b84f9c5f..e9297d8c 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/Enums/UtilityName.cs @@ -1,13 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace BotSharp.Plugin.EmailHandler.Enums; -namespace BotSharp.Plugin.EmailHandler.Enums +public class UtilityName { - public class UtilityName - { - public const string EmailHandler = "email-handler"; - } + public const string EmailHandler = "email.email-handler"; } diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs index d0f11466..1833a783 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailReaderFn.cs @@ -7,7 +7,7 @@ namespace BotSharp.Plugin.EmailReader.Functions; public class HandleEmailReaderFn : IFunctionCallback { - public string Name => "handle_email_reader"; + public string Name => "util-email-handle_email_reader"; public readonly static string PROMPT_SUMMARY = "Provide a text summary of the following content."; public readonly static string RICH_CONTENT_SUMMARIZE = "is_email_summarize: true. messageId"; public readonly static string RICH_CONTENT_READ_EMAIL = "Read the email by messageId"; diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailSenderFn.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailSenderFn.cs index fa038811..a6b04e49 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailSenderFn.cs +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/Functions/HandleEmailSenderFn.cs @@ -6,7 +6,7 @@ namespace BotSharp.Plugin.EmailHandler.Functions; public class HandleEmailSenderFn : IFunctionCallback { - public string Name => "handle_email_sender"; + public string Name => "util-email-handle_email_sender"; public string Indication => "Handling email send request"; private readonly IServiceProvider _services; diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailHandlerUtilityHook.cs index 1d801e66..a99dfc8c 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailHandlerUtilityHook.cs @@ -1,18 +1,23 @@ using BotSharp.Abstraction.Agents; using BotSharp.Plugin.EmailHandler.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace BotSharp.Plugin.EmailHandler.Hooks +namespace BotSharp.Plugin.EmailHandler.Hooks; + +public class EmailHandlerUtilityHook : IAgentUtilityHook { - public class EmailHandlerUtilityHook : IAgentUtilityHook + private static string PREFIX = "util-email-"; + private static string EMAIL_READER_FN = $"{PREFIX}handle_email_reader"; + private static string EMAIL_SENDER_FN = $"{PREFIX}handle_email_sender"; + + public void AddUtilities(List utilities) { - public void AddUtilities(List utilities) + var utility = new AgentUtility { - utilities.Add(UtilityName.EmailHandler); - } + Name = UtilityName.EmailHandler, + Functions = [new(EMAIL_READER_FN), new(EMAIL_SENDER_FN)], + Templates = [new($"{EMAIL_READER_FN}.fn"), new($"{EMAIL_SENDER_FN}.fn")] + }; + + utilities.Add(utility); } } diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailReaderHook.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailReaderHook.cs deleted file mode 100644 index beece9e2..00000000 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailReaderHook.cs +++ /dev/null @@ -1,56 +0,0 @@ -using BotSharp.Abstraction.Agents; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Plugin.EmailHandler.Enums; - -namespace BotSharp.Plugin.EmailHandler.Hooks; - -public class EmailReaderHook : AgentHookBase -{ - private static string FUNCTION_NAME = "handle_email_reader"; - - public override string SelfId => string.Empty; - - public EmailReaderHook(IServiceProvider services, AgentSettings settings) - : base(services, settings) - { - } - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.EmailHandler); - - if (isConvMode && isEnabled) - { - var (prompt, fn) = GetPromptAndFunction(); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, FunctionDef?) GetPromptAndFunction() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME)); - return (prompt, loadAttachmentFn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailSenderHook.cs b/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailSenderHook.cs deleted file mode 100644 index 27491015..00000000 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/Hooks/EmailSenderHook.cs +++ /dev/null @@ -1,56 +0,0 @@ -using BotSharp.Abstraction.Agents; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Plugin.EmailHandler.Enums; - -namespace BotSharp.Plugin.EmailHandler.Hooks; - -public class EmailSenderHook : AgentHookBase -{ - private static string FUNCTION_NAME = "handle_email_sender"; - - public override string SelfId => string.Empty; - - public EmailSenderHook(IServiceProvider services, AgentSettings settings) - : base(services, settings) - { - } - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.EmailHandler); - - if (isConvMode && isEnabled) - { - var (prompt, fn) = GetPromptAndFunction(); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, FunctionDef?) GetPromptAndFunction() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME)); - return (prompt, loadAttachmentFn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_email_reader.json b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-email-handle_email_reader.json similarity index 95% rename from src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_email_reader.json rename to src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-email-handle_email_reader.json index bcea2ac8..36e86402 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_email_reader.json +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-email-handle_email_reader.json @@ -1,5 +1,5 @@ { - "name": "handle_email_reader", + "name": "util-email-handle_email_reader", "description": "If the user wants to read messages from email inbox or user wants to mark an email message as read.If message id is provided, capture it but it is not required. Then call this function to read the email message or mark any message as read.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_email_sender.json b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-email-handle_email_sender.json similarity index 96% rename from src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_email_sender.json rename to src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-email-handle_email_sender.json index 730c4597..b1ebfb73 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_email_sender.json +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-email-handle_email_sender.json @@ -1,5 +1,5 @@ { - "name": "handle_email_sender", + "name": "util-email-handle_email_sender", "description": "If the user requests to send an email with or without attachments or files, you need to capture the email content and the recipient email address. If the user explicitly enter email subject use the same if not intelligently capture the email subject from the content. Then call this function to send out email.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_email_reader.fn.liquid b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_email_reader.fn.liquid deleted file mode 100644 index 0efb6229..00000000 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_email_reader.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Please call handle_email_reader if user wants to read messages from email inbox. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-email-handle_email_reader.fn.liquid b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-email-handle_email_reader.fn.liquid new file mode 100644 index 00000000..10708bb7 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-email-handle_email_reader.fn.liquid @@ -0,0 +1 @@ +Please call util-email-handle_email_reader if user wants to read messages from email inbox. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_email_sender.fn.liquid b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-email-handle_email_sender.fn.liquid similarity index 81% rename from src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_email_sender.fn.liquid rename to src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-email-handle_email_sender.fn.liquid index eb6f0e6f..5fc29e3d 100644 --- a/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_email_sender.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.EmailHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-email-handle_email_sender.fn.liquid @@ -1,4 +1,4 @@ ** Please take a look at the conversation and decide whether user wants to send email. ** If yes, then decide whether user wants to send email with files/attachments/images or not. ** If user does not want to send email explicitly or only upload images or files, please do not respond anything email related message. -** Please call handle_email_sender if user wants to send email. \ No newline at end of file +** Please call util-email-handle_email_sender if user wants to send email. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj b/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj index a57e28b0..08cfd79a 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/BotSharp.Plugin.ExcelHandler.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -13,22 +13,21 @@ - - + + - + PreserveNewest - + PreserveNewest - diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/Enums/UtilityName.cs index 66ef3f36..bc689259 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/Enums/UtilityName.cs @@ -2,5 +2,5 @@ namespace BotSharp.Plugin.ExcelHandler.Enums; public class UtilityName { - public const string ExcelHandler = "excel-handler"; + public const string ExcelHandler = "excel.excel-handler"; } diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/ExcelHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/ExcelHandlerPlugin.cs index 0111ea99..08d652f0 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/ExcelHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/ExcelHandlerPlugin.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using BotSharp.Abstraction.Plugins; using BotSharp.Abstraction.Settings; using BotSharp.Plugin.ExcelHandler.Helpers.MySql; @@ -30,7 +25,6 @@ public class ExcelHandlerPlugin : IBotSharpPlugin }); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/Functions/HandleExcelRequestFn.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/Functions/HandleExcelRequestFn.cs index f72492e3..270f6177 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/Functions/HandleExcelRequestFn.cs +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/Functions/HandleExcelRequestFn.cs @@ -2,7 +2,6 @@ using System.Linq.Dynamic.Core; using BotSharp.Abstraction.Files.Enums; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; -using BotSharp.Plugin.ExcelHandler.Helpers.Sqlite; using BotSharp.Plugin.ExcelHandler.Models; using BotSharp.Plugin.ExcelHandler.Services; using NPOI.SS.UserModel; @@ -12,7 +11,7 @@ namespace BotSharp.Plugin.ExcelHandler.Functions; public class HandleExcelRequestFn : IFunctionCallback { - public string Name => "handle_excel_request"; + public string Name => "util-excel-handle_excel_request"; public string Indication => "Handling excel request"; private readonly IServiceProvider _serviceProvider; diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/MySql/MySqlDbHelpers.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/MySql/MySqlDbHelpers.cs index 4b86f89f..8b4c0940 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/MySql/MySqlDbHelpers.cs +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/MySql/MySqlDbHelpers.cs @@ -1,12 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using BotSharp.Plugin.SqlHero.Settings; -using Microsoft.Data.Sqlite; +using BotSharp.Plugin.SqlDriver.Settings; using MySql.Data.MySqlClient; namespace BotSharp.Plugin.ExcelHandler.Helpers.MySql diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/Sqlite/SqliteDbHelpers.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/Sqlite/SqliteDbHelpers.cs index c55e1642..464fa467 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/Sqlite/SqliteDbHelpers.cs +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/Helpers/Sqlite/SqliteDbHelpers.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using BotSharp.Plugin.SqlDriver.Settings; using Microsoft.Data.Sqlite; -using BotSharp.Plugin.SqlDriver.Models; -using BotSharp.Plugin.SqlHero.Settings; namespace BotSharp.Plugin.ExcelHandler.Helpers.Sqlite; diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerHook.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerHook.cs deleted file mode 100644 index 8716e4b1..00000000 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerHook.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace BotSharp.Plugin.ExcelHandler.Hooks; - -public class ExcelHandlerHook : AgentHookBase, IAgentHook -{ - private const string HANDLER_EXCEL = "handle_excel_request"; - - public override string SelfId => string.Empty; - - public ExcelHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.ExcelHandler); - - if (isEnabled && isConvMode) - { - AddUtility(agent, HANDLER_EXCEL); - } - - base.OnAgentLoaded(agent); - } - - private void AddUtility(Agent agent, string functionName) - { - var (prompt, fn) = GetPromptAndFunction(functionName); - - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - private (string, FunctionDef?) GetPromptAndFunction(string functionName) - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName)); - return (prompt, loadAttachmentFn); - } -} - diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerUtilityHook.cs index fde7533f..6293a832 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/Hooks/ExcelHandlerUtilityHook.cs @@ -2,8 +2,18 @@ namespace BotSharp.Plugin.ExcelHandler.Hooks; public class ExcelHandlerUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private static string PREFIX = "util-excel-"; + private static string HANDLER_EXCEL = $"{PREFIX}handle_excel_request"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.ExcelHandler); + var utility = new AgentUtility + { + Name = UtilityName.ExcelHandler, + Functions = [new(HANDLER_EXCEL)], + Templates = [new($"{HANDLER_EXCEL}.fn")] + }; + + utilities.Add(utility); } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_excel_request.json b/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-excel-handle_excel_request.json similarity index 95% rename from src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_excel_request.json rename to src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-excel-handle_excel_request.json index 38f2faab..53381c67 100644 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_excel_request.json +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-excel-handle_excel_request.json @@ -1,5 +1,5 @@ { - "name": "handle_excel_request", + "name": "util-excel-handle_excel_request", "description": "If the user requests to read/load data from excel/csv files, you need to call this function to load the data from excel/csv files and transform into JSON format data", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_excel_request.fn.liquid b/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_excel_request.fn.liquid deleted file mode 100644 index a3f27694..00000000 --- a/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_excel_request.fn.liquid +++ /dev/null @@ -1,2 +0,0 @@ -Please call handle_excel_request if user wants to load the data from a excel/csv file. -handle_excel_request can NOT generate excel/csv. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-excel-handle_excel_request.fn.liquid b/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-excel-handle_excel_request.fn.liquid new file mode 100644 index 00000000..53aef674 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.ExcelHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-excel-handle_excel_request.fn.liquid @@ -0,0 +1,2 @@ +Please call util-excel-handle_excel_request if user wants to load the data from a excel/csv file. +util-excel-handle_excel_request can NOT generate excel/csv. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj b/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj index 9b097fed..2147d178 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.FileHandler/BotSharp.Plugin.FileHandler.csproj @@ -11,40 +11,43 @@ - - - - - - - - + + + + + + + + + + + - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Enums/UtilityName.cs index eb2b4e2b..68242202 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Enums/UtilityName.cs @@ -2,8 +2,8 @@ namespace BotSharp.Plugin.FileHandler.Enums; public class UtilityName { - public const string ImageGenerator = "image-generator"; - public const string ImageReader = "image-reader"; - public const string ImageEditor = "image-editor"; - public const string PdfReader = "pdf-reader"; + public const string ImageGenerator = "file.image-generator"; + public const string ImageReader = "file.image-reader"; + public const string ImageEditor = "file.image-editor"; + public const string PdfReader = "file.pdf-reader"; } diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/FileHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.FileHandler/FileHandlerPlugin.cs index caef8304..a8ecac6d 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/FileHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/FileHandlerPlugin.cs @@ -19,7 +19,6 @@ public class FileHandlerPlugin : IBotSharpPlugin return settingService.Bind("FileHandler"); }); - services.AddScoped(); services.AddScoped(); } diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs index cb7139f0..87d8268b 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/EditImageFn.cs @@ -4,7 +4,7 @@ namespace BotSharp.Plugin.FileHandler.Functions; public class EditImageFn : IFunctionCallback { - public string Name => "edit_image"; + public string Name => "util-file-edit_image"; public string Indication => "Editing image"; private readonly IServiceProvider _services; diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs index c3e8c147..a511ebae 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/GenerateImageFn.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.FileHandler.Functions; public class GenerateImageFn : IFunctionCallback { - public string Name => "generate_image"; + public string Name => "util-file-generate_image"; public string Indication => "Generating image"; private readonly IServiceProvider _services; diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadImageFn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadImageFn.cs index a415207e..e2c4f8c2 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadImageFn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadImageFn.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.FileHandler.Functions; public class ReadImageFn : IFunctionCallback { - public string Name => "read_image"; + public string Name => "util-file-read_image"; public string Indication => "Reading images"; private readonly IServiceProvider _services; @@ -23,8 +23,9 @@ public class ReadImageFn : IFunctionCallback var agentService = _services.GetRequiredService(); var wholeDialogs = conv.GetDialogHistory(); - var dialogs = AssembleFiles(conv.ConversationId, wholeDialogs); - var agent = await agentService.LoadAgent(BuiltInAgentId.UtilityAssistant); + var dialogs = AssembleFiles(conv.ConversationId, args?.ImageUrls, wholeDialogs); + var agentId = !string.IsNullOrWhiteSpace(message.CurrentAgentId) ? message.CurrentAgentId : BuiltInAgentId.UtilityAssistant; + var agent = await agentService.LoadAgent(agentId); var fileAgent = new Agent { Id = agent?.Id ?? Guid.Empty.ToString(), @@ -38,7 +39,7 @@ public class ReadImageFn : IFunctionCallback return true; } - private List AssembleFiles(string conversationId, List dialogs) + private List AssembleFiles(string conversationId, IEnumerable? imageUrls, List dialogs) { if (dialogs.IsNullOrEmpty()) { @@ -66,6 +67,18 @@ public class ReadImageFn : IFunctionCallback }).ToList(); } + if (!imageUrls.IsNullOrEmpty()) + { + var lastDialog = dialogs.Last(); + var files = lastDialog.Files ?? []; + var addnFiles = imageUrls.Select(x => x?.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => new BotSharpFile { FileUrl = x }).ToList(); + + files.AddRange(addnFiles); + lastDialog.Files = files; + } + return dialogs; } diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadPdfFn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadPdfFn.cs index 814f943e..2aeb64a1 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadPdfFn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Functions/ReadPdfFn.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.FileHandler.Functions; public class ReadPdfFn : IFunctionCallback { - public string Name => "read_pdf"; + public string Name => "util-file-read_pdf"; public string Indication => "Reading pdf"; private readonly IServiceProvider _services; diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerHook.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerHook.cs deleted file mode 100644 index e9375244..00000000 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerHook.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace BotSharp.Plugin.FileHandler.Hooks; - -public class FileHandlerHook : AgentHookBase, IAgentHook -{ - private const string READ_IMAGE_FN = "read_image"; - private const string READ_PDF_FN = "read_pdf"; - private const string GENERATE_IMAGE_FN = "generate_image"; - private const string EDIT_IMAGE_FN = "edit_image"; - - public override string SelfId => string.Empty; - - public FileHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - - if (isConvMode) - { - AddUtility(agent, UtilityName.ImageGenerator, GENERATE_IMAGE_FN); - AddUtility(agent, UtilityName.ImageReader, READ_IMAGE_FN); - AddUtility(agent, UtilityName.ImageEditor, EDIT_IMAGE_FN); - AddUtility(agent, UtilityName.PdfReader, READ_PDF_FN); - - } - - base.OnAgentLoaded(agent); - } - - private void AddUtility(Agent agent, string utility, string functionName) - { - if (!IsEnableUtility(agent, utility)) return; - - var (prompt, fn) = GetPromptAndFunction(functionName); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - private bool IsEnableUtility(Agent agent, string utility) - { - return !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(utility); - } - - private (string, FunctionDef?) GetPromptAndFunction(string functionName) - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName)); - return (prompt, loadAttachmentFn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerUtilityHook.cs index ce28634f..3409c448 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/Hooks/FileHandlerUtilityHook.cs @@ -2,11 +2,41 @@ namespace BotSharp.Plugin.FileHandler.Hooks; public class FileHandlerUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private const string READ_IMAGE_FN = "util-file-read_image"; + private const string READ_PDF_FN = "util-file-read_pdf"; + private const string GENERATE_IMAGE_FN = "util-file-generate_image"; + private const string EDIT_IMAGE_FN = "util-file-edit_image"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.ImageGenerator); - utilities.Add(UtilityName.ImageReader); - utilities.Add(UtilityName.ImageEditor); - utilities.Add(UtilityName.PdfReader); + var items = new List + { + new AgentUtility + { + Name = UtilityName.ImageGenerator, + Functions = [new(GENERATE_IMAGE_FN)], + Templates = [new($"{GENERATE_IMAGE_FN}.fn")] + }, + new AgentUtility + { + Name = UtilityName.ImageReader, + Functions = [new(READ_IMAGE_FN)], + Templates = [new($"{READ_IMAGE_FN}.fn")] + }, + new AgentUtility + { + Name = UtilityName.ImageEditor, + Functions = [new(EDIT_IMAGE_FN)], + Templates = [new($"{EDIT_IMAGE_FN}.fn")] + }, + new AgentUtility + { + Name = UtilityName.PdfReader, + Functions = [new(READ_PDF_FN)], + Templates = [new($"{READ_PDF_FN}.fn")] + } + }; + + utilities.AddRange(items); } } diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs index 9917cb74..a35e83bd 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs +++ b/src/Plugins/BotSharp.Plugin.FileHandler/LlmContexts/LlmContextIn.cs @@ -11,4 +11,12 @@ public class LlmContextIn [JsonPropertyName("image_description")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ImageDescription { get; set; } + + //[JsonPropertyName("image_url")] + //[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + //public string? ImageUrl { get; set; } + + [JsonPropertyName("image_urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IEnumerable? ImageUrls { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/read_image.json b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/read_image.json deleted file mode 100644 index 99daafc5..00000000 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/read_image.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "read_image", - "description": "If the user's request is related to analyzing images, you can call this function to analyze images.", - "parameters": { - "type": "object", - "properties": { - "user_request": { - "type": "string", - "description": "The request posted by user, which is related to analyzing requested images. User can request for multiple images to process at one time." - } - }, - "required": [ "user_request" ] - } -} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/edit_image.json b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-edit_image.json similarity index 92% rename from src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/edit_image.json rename to src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-edit_image.json index f513ffba..306e44dd 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/edit_image.json +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-edit_image.json @@ -1,5 +1,5 @@ { - "name": "edit_image", + "name": "util-file-edit_image", "description": "If the user requests you editting or changing an image or a picture, you can call this function to edit an image.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/generate_image.json b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-generate_image.json similarity index 90% rename from src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/generate_image.json rename to src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-generate_image.json index d9c8b70e..ba96aef0 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/generate_image.json +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-generate_image.json @@ -1,5 +1,5 @@ { - "name": "generate_image", + "name": "util-file-generate_image", "description": "If the user requests you providing or generating image or picture, you can call this function to generate image.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-read_image.json b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-read_image.json new file mode 100644 index 00000000..b8dda8b1 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-read_image.json @@ -0,0 +1,22 @@ +{ + "name": "util-file-read_image", + "description": "If the user's request is related to analyzing images, you can call this function to analyze images.", + "parameters": { + "type": "object", + "properties": { + "user_request": { + "type": "string", + "description": "The request posted by user, which is related to analyzing requested images. User can request for multiple images to process at one time." + }, + "image_urls": { + "type": "array", + "description": "The image, photo or picture urls that user requests for analysis. They typically start with 'http' or 'https'. If user doesn't include any url, then leave this array empty. Please remove any duplicated urls", + "items": { + "type": "string", + "description": "The image, photo or picture url that user requests for analysis. It typically starts with http or https." + } + } + }, + "required": [ "user_request" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/read_pdf.json b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-read_pdf.json similarity index 92% rename from src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/read_pdf.json rename to src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-read_pdf.json index 54d9424c..d6b27819 100644 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/read_pdf.json +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-file-read_pdf.json @@ -1,5 +1,5 @@ { - "name": "read_pdf", + "name": "util-file-read_pdf", "description": "If the user's request is related to analyzing pdf files, you can call this function to analyze pdf.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/edit_image.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/edit_image.fn.liquid deleted file mode 100644 index 7fa2ac21..00000000 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/edit_image.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Please call edit_image if user wants to edit or change an image in the conversation. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/generate_image.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/generate_image.fn.liquid deleted file mode 100644 index 9ee28f9c..00000000 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/generate_image.fn.liquid +++ /dev/null @@ -1,2 +0,0 @@ -** Please call generate_image if user wants you to provide or generate an image or picture. -** If user does not generate image explicitly, please do not call generate_image. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/read_image.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/read_image.fn.liquid deleted file mode 100644 index 3ebf28cc..00000000 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/read_image.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Please call function read_image if user wants to describe an image or images. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/read_pdf.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/read_pdf.fn.liquid deleted file mode 100644 index 0ea6706d..00000000 --- a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/read_pdf.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Please call function read_pdf if user wants to describe an pdf file or pdf files. diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-edit_image.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-edit_image.fn.liquid new file mode 100644 index 00000000..49ab707f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-edit_image.fn.liquid @@ -0,0 +1 @@ +Please call util-file-edit_image if user wants to edit or change an image in the conversation. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-generate_image.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-generate_image.fn.liquid new file mode 100644 index 00000000..6d7dd619 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-generate_image.fn.liquid @@ -0,0 +1,2 @@ +** Please call util-file-generate_image if user wants you to provide or generate an image or picture. +** If user does not generate image explicitly, please do not call generate_image. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-read_image.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-read_image.fn.liquid new file mode 100644 index 00000000..76f75bd0 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-read_image.fn.liquid @@ -0,0 +1 @@ +Please call function util-file-read_image if user wants to describe an image or images. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-read_pdf.fn.liquid b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-read_pdf.fn.liquid new file mode 100644 index 00000000..12d08f0a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.FileHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-file-read_pdf.fn.liquid @@ -0,0 +1 @@ +Please call function util-file-read_pdf if user wants to describe an pdf file or pdf files. diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj index 8664c654..7dc0ac87 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj @@ -11,11 +11,11 @@ - - + + @@ -25,10 +25,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Enums/UtilityName.cs index 2e0146bf..3a1e7cd5 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Enums/UtilityName.cs @@ -2,5 +2,5 @@ namespace BotSharp.Plugin.HttpHandler.Enums; public class UtilityName { - public const string HttpHandler = "http-handler"; + public const string HttpHandler = "http.http-handler"; } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequestFn.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequestFn.cs index 20d2d012..d36aafba 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequestFn.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequestFn.cs @@ -7,7 +7,7 @@ namespace BotSharp.Plugin.HttpHandler.Functions; public class HandleHttpRequestFn : IFunctionCallback { - public string Name => "handle_http_request"; + public string Name => "util-http-handle_http_request"; public string Indication => "Handling http request"; private readonly IServiceProvider _services; diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerHook.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerHook.cs deleted file mode 100644 index 78ee2c09..00000000 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerHook.cs +++ /dev/null @@ -1,58 +0,0 @@ -using BotSharp.Abstraction.Agents; -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; - -namespace BotSharp.Plugin.HttpHandler.Hooks; - -public class HttpHandlerHook : AgentHookBase -{ - private static string FUNCTION_NAME = "handle_http_request"; - - public override string SelfId => string.Empty; - - public HttpHandlerHook(IServiceProvider services, AgentSettings settings) - : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.HttpHandler); - - if (isConvMode && isEnabled) - { - var (prompt, fn) = GetPromptAndFunction(FUNCTION_NAME); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, FunctionDef?) GetPromptAndFunction(string functionName) - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName)); - return (prompt, loadAttachmentFn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerUtilityHook.cs index 17a38df5..d99b6084 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Hooks/HttpHandlerUtilityHook.cs @@ -4,8 +4,18 @@ namespace BotSharp.Plugin.HttpHandler.Hooks; public class HttpHandlerUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private static string PREFIX = "util-http-"; + private static string HTTP_HANDLER_FN = $"{PREFIX}handle_http_request"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.HttpHandler); + var utility = new AgentUtility + { + Name = UtilityName.HttpHandler, + Functions = [new(HTTP_HANDLER_FN)], + Templates = [new($"{HTTP_HANDLER_FN}.fn")] + }; + + utilities.Add(utility); } } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs index 48d71ee8..9c8cab55 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs @@ -20,7 +20,6 @@ public class HttpHandlerPlugin : IBotSharpPlugin return settingService.Bind("HttpHandler"); }); - services.AddScoped(); services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_http_request.json b/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-http-handle_http_request.json similarity index 95% rename from src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_http_request.json rename to src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-http-handle_http_request.json index c7335cee..40ce00d5 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/handle_http_request.json +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-http-handle_http_request.json @@ -1,5 +1,5 @@ { - "name": "handle_http_request", + "name": "util-http-handle_http_request", "description": "If the user requests to send an http request, you need to capture the http method and request content, and then call this function to send the http request.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_http_request.fn.liquid b/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_http_request.fn.liquid deleted file mode 100644 index 7640d81e..00000000 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/handle_http_request.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Please call handle_http_request if user wants to send an http request. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-http-handle_http_request.fn.liquid b/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-http-handle_http_request.fn.liquid new file mode 100644 index 00000000..2d639dd0 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-http-handle_http_request.fn.liquid @@ -0,0 +1 @@ +Please call util-http-handle_http_request if user wants to send an http request. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj b/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj new file mode 100644 index 00000000..4596ce1b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/BotSharp.Plugin.JavaScriptInterpreter.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/JsInterpreterPlugin.cs b/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/JsInterpreterPlugin.cs new file mode 100644 index 00000000..e3811809 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.JavaScriptInterpreter/JsInterpreterPlugin.cs @@ -0,0 +1,24 @@ +using BotSharp.Abstraction.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BotSharp.Plugin.JavaScriptInterpreter; + +public class JsInterpreterPlugin : IBotSharpAppPlugin +{ + public string Id => "7a5a8cd7-26d9-4ac3-9d79-d02084bea372"; + public string Name => "JavaScript Interpreter"; + public string Description => ""; + public string? IconUrl => ""; + + public void Configure(IApplicationBuilder app) + { + + } + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + + } +} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj b/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj index 283d8b32..77a5d076 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/BotSharp.Plugin.KnowledgeBase.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -23,7 +23,8 @@ - + + @@ -45,10 +46,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs index 18a28c91..de90430d 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Enum/UtilityName.cs @@ -2,5 +2,5 @@ namespace BotSharp.Plugin.KnowledgeBase.Enum; public class UtilityName { - public const string KnowledgeRetrieval = "knowledge-retrieval"; + public const string KnowledgeRetrieval = "knowledge.knowledge-retrieval"; } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs index 606caad9..f3f8cbf1 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Functions/KnowledgeRetrievalFn.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.KnowledgeBase.Functions; public class KnowledgeRetrievalFn : IFunctionCallback { - public string Name => "knowledge_retrieval"; + public string Name => "util-knowledge-knowledge_retrieval"; public string Indication => "searching my brain"; diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseAgentHook.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseAgentHook.cs deleted file mode 100644 index f8296520..00000000 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseAgentHook.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace BotSharp.Plugin.KnowledgeBase.Hooks; - -public class KnowledgeBaseAgentHook : AgentHookBase, IAgentHook -{ - public override string SelfId => string.Empty; - public KnowledgeBaseAgentHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - - if (isConvMode) - { - AddUtility(agent, UtilityName.KnowledgeRetrieval, "knowledge_retrieval"); - } - - base.OnAgentLoaded(agent); - } - - private void AddUtility(Agent agent, string utility, string functionName) - { - if (!IsEnableUtility(agent, utility)) return; - - var (prompt, fn) = GetPromptAndFunction(functionName); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - private bool IsEnableUtility(Agent agent, string utility) - { - return !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(utility); - } - - private (string, FunctionDef?) GetPromptAndFunction(string functionName) - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty; - var fn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName)); - return (prompt, fn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs index cd428136..700b8457 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/Hooks/KnowledgeBaseUtilityHook.cs @@ -2,8 +2,18 @@ namespace BotSharp.Plugin.KnowledgeBase.Hooks; public class KnowledgeBaseUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private static string PREFIX = "util-knowledge-"; + private static string KNOWLEDGE_RETRIEVAL_FN = $"{PREFIX}knowledge_retrieval"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.KnowledgeRetrieval); + var utility = new AgentUtility + { + Name = UtilityName.KnowledgeRetrieval, + Functions = [new(KNOWLEDGE_RETRIEVAL_FN)], + Templates = [new($"{KNOWLEDGE_RETRIEVAL_FN}.fn")] + }; + + utilities.Add(utility); } } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs index ebb5aa63..8d89daf6 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs @@ -25,7 +25,6 @@ public class KnowledgeBasePlugin : IBotSharpPlugin services.AddSingleton(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); } diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/knowledge_retrieval.json b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-knowledge-knowledge_retrieval.json similarity index 86% rename from src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/knowledge_retrieval.json rename to src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-knowledge-knowledge_retrieval.json index b292d7d9..088f4ea6 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/knowledge_retrieval.json +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-knowledge-knowledge_retrieval.json @@ -1,5 +1,5 @@ { - "name": "knowledge_retrieval", + "name": "util-knowledge-knowledge_retrieval", "description": "Retrieve related domain knowledge to handle user request", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/knowledge_retrieval.fn.liquid b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-knowledge-knowledge_retrieval.fn.liquid similarity index 63% rename from src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/knowledge_retrieval.fn.liquid rename to src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-knowledge-knowledge_retrieval.fn.liquid index da5e157b..f0bc60c8 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/knowledge_retrieval.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-knowledge-knowledge_retrieval.fn.liquid @@ -1,3 +1,3 @@ -Call function knowledge_retrieval to retrieve related domain knowledge to handle user request. +Call function util-knowledge-knowledge_retrieval to retrieve related domain knowledge to handle user request. You must retrieve existing KnowledgeBase to get prerequisite informations before you writing SQL query; You must retrieve existing API specification from KnowledgeBase before calling a Web API; diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj b/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj index bd7cf7b7..68ce651c 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/BotSharp.Plugin.LLamaSharp.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/LlamaAiModel.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/LlamaAiModel.cs index 9d55dead..a7d83def 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/LlamaAiModel.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/LlamaAiModel.cs @@ -36,7 +36,6 @@ public class LlamaAiModel _params = new ModelParams(Path.Combine(_settings.ModelDir, model)) { ContextSize = (uint)_settings.MaxContextLength, - Seed = 1337, GpuLayerCount = _settings.NumberOfGpuLayer }; diff --git a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs index a9288064..1ec461ae 100644 --- a/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs +++ b/src/Plugins/BotSharp.Plugin.LLamaSharp/Providers/TextEmbeddingProvider.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Xml.Linq; +using static System.Net.Mime.MediaTypeNames; namespace BotSharp.Plugin.LLamaSharp.Providers; @@ -19,7 +21,7 @@ public class TextEmbeddingProvider : ITextEmbedding _settings = settings; } - public Task GetVectorAsync(string text) + public async Task GetVectorAsync(string text) { if (_embedder == null) { @@ -29,10 +31,10 @@ public class TextEmbeddingProvider : ITextEmbedding _embedder = new LLamaEmbedder(weights, @params); } - return _embedder.GetEmbeddings(text); + return (await _embedder.GetEmbeddings(text)).First(); } - public Task> GetVectorsAsync(List texts) + public async Task> GetVectorsAsync(List texts) { throw new NotImplementedException(); } diff --git a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/BotSharp.Plugin.MicrosoftExtensionsAI.csproj b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/BotSharp.Plugin.MicrosoftExtensionsAI.csproj index 9262828f..9ce69188 100644 --- a/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/BotSharp.Plugin.MicrosoftExtensionsAI.csproj +++ b/src/Plugins/BotSharp.Plugin.MicrosoftExtensionsAI/BotSharp.Plugin.MicrosoftExtensionsAI.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj b/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj index ab14d864..88709238 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/BotSharp.Plugin.MongoStorage.csproj @@ -11,8 +11,7 @@ - - + diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs index baa0729f..3a775025 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs @@ -8,14 +8,16 @@ public class AgentDocument : MongoBase public string? InheritAgentId { get; set; } public string? IconUrl { get; set; } public string Instruction { get; set; } + public bool IsPublic { get; set; } + public bool Disabled { get; set; } + public bool MergeUtility { get; set; } + public int? MaxMessageCount { get; set; } public List ChannelInstructions { get; set; } public List Templates { get; set; } public List Functions { get; set; } public List Responses { get; set; } public List Samples { get; set; } - public List Utilities { get; set; } - public bool IsPublic { get; set; } - public bool Disabled { get; set; } + public List Utilities { get; set; } public List Profiles { get; set; } public List RoutingRules { get; set; } public AgentLlmConfigMongoElement? LlmConfig { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs index 28b0391a..52c26b52 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/ConversationDocument.cs @@ -6,6 +6,7 @@ public class ConversationDocument : MongoBase public string UserId { get; set; } public string? TaskId { get; set; } public string Title { get; set; } + public string TitleAlias { get; set; } public string Channel { get; set; } public string ChannelId { get; set; } public string Status { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/CrontabItemDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/CrontabItemDocument.cs new file mode 100644 index 00000000..39622d4c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/CrontabItemDocument.cs @@ -0,0 +1,57 @@ +using BotSharp.Abstraction.Crontab.Models; + +namespace BotSharp.Plugin.MongoStorage.Collections; + +public class CrontabItemDocument : MongoBase +{ + public string UserId { get; set; } + public string AgentId { get; set; } + public string ConversationId { get; set; } + public string ExecutionResult { get; set; } + public string Cron { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public int ExecutionCount { get; set; } + public int MaxExecutionCount { get; set; } + public int ExpireSeconds { get; set; } + public IEnumerable Tasks { get; set; } = []; + public DateTime CreatedTime { get; set; } = DateTime.UtcNow; + + public static CrontabItem ToDomainModel(CrontabItemDocument item) + { + return new CrontabItem + { + UserId = item.UserId, + AgentId = item.AgentId, + ConversationId = item.ConversationId, + ExecutionResult = item.ExecutionResult, + Cron = item.Cron, + Title = item.Title, + Description = item.Description, + ExecutionCount = item.ExecutionCount, + MaxExecutionCount = item.MaxExecutionCount, + ExpireSeconds = item.ExpireSeconds, + Tasks = item.Tasks?.Select(x => CronTaskMongoElement.ToDomainElement(x))?.ToArray() ?? [], + CreatedTime = item.CreatedTime + }; + } + + public static CrontabItemDocument ToMongoModel(CrontabItem item) + { + return new CrontabItemDocument + { + UserId = item.UserId, + AgentId = item.AgentId, + ConversationId = item.ConversationId, + ExecutionResult = item.ExecutionResult, + Cron = item.Cron, + Title = item.Title, + Description = item.Description, + ExecutionCount = item.ExecutionCount, + MaxExecutionCount = item.MaxExecutionCount, + ExpireSeconds = item.ExpireSeconds, + Tasks = item.Tasks?.Select(x => CronTaskMongoElement.ToMongoElement(x))?.ToList() ?? [], + CreatedTime = item.CreatedTime + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs index 4054d09d..4c716959 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs @@ -26,6 +26,8 @@ public class UserDocument : MongoBase public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } + public Dashboard? Dashboard { get; set; } + public User ToUser() { return new User @@ -49,6 +51,8 @@ public class UserDocument : MongoBase Verified = Verified, RegionCode = RegionCode, Permissions = Permissions, + CreatedTime = CreatedTime, + UpdatedTime = UpdatedTime, }; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentUtilityMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentUtilityMongoElement.cs new file mode 100644 index 00000000..c05f07ef --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentUtilityMongoElement.cs @@ -0,0 +1,63 @@ +using BotSharp.Abstraction.Agents.Models; + +namespace BotSharp.Plugin.MongoStorage.Models; + +public class AgentUtilityMongoElement +{ + public string Name { get; set; } + public bool Disabled { get; set; } + public List Functions { get; set; } = []; + public List Templates { get; set; } = []; + + public static AgentUtilityMongoElement ToMongoElement(AgentUtility utility) + { + return new AgentUtilityMongoElement + { + Name = utility.Name, + Disabled = utility.Disabled, + Functions = utility.Functions?.Select(x => new UtilityFunctionMongoElement(x.Name))?.ToList() ?? [], + Templates = utility.Templates?.Select(x => new UtilityTemplateMongoElement(x.Name))?.ToList() ?? [] + }; + } + + public static AgentUtility ToDomainElement(AgentUtilityMongoElement utility) + { + return new AgentUtility + { + Name = utility.Name, + Disabled = utility.Disabled, + Functions = utility.Functions?.Select(x => new UtilityFunction(x.Name))?.ToList() ?? [], + Templates = utility.Templates?.Select(x => new UtilityTemplate(x.Name))?.ToList() ?? [] + }; + } +} + +public class UtilityFunctionMongoElement +{ + public string Name { get; set; } + + public UtilityFunctionMongoElement() + { + + } + + public UtilityFunctionMongoElement(string name) + { + Name = name; + } +} + +public class UtilityTemplateMongoElement +{ + public string Name { get; set; } + + public UtilityTemplateMongoElement() + { + + } + + public UtilityTemplateMongoElement(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/CronTaskMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/CronTaskMongoElement.cs new file mode 100644 index 00000000..099286b8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/CronTaskMongoElement.cs @@ -0,0 +1,30 @@ +using BotSharp.Abstraction.Crontab.Models; + +namespace BotSharp.Plugin.MongoStorage.Models; + +public class CronTaskMongoElement +{ + public string Topic { get; set; } + public string Script { get; set; } + public string Language { get; set; } + + public static CronTaskMongoElement ToMongoElement(ScheduleTaskItemArgs model) + { + return new CronTaskMongoElement + { + Topic = model.Topic, + Script = model.Script, + Language = model.Language + }; + } + + public static ScheduleTaskItemArgs ToDomainElement(CronTaskMongoElement model) + { + return new ScheduleTaskItemArgs + { + Topic = model.Topic, + Script = model.Script, + Language = model.Language + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs index b91c35fe..89eb18ad 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoDbContext.cs @@ -165,4 +165,7 @@ public class MongoDbContext public IMongoCollection RoleAgents => Database.GetCollection($"{_collectionPrefix}_RoleAgents"); + + public IMongoCollection CrontabItems + => Database.GetCollection($"{_collectionPrefix}_CronTabItems"); } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index d17f7f2f..9bd72e3e 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -2,7 +2,6 @@ using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions.Models; using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Routing.Models; -using MongoDB.Driver; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -10,7 +9,7 @@ public partial class MongoRepository { public void UpdateAgent(Agent agent, AgentField field) { - if (agent == null || string.IsNullOrEmpty(agent.Id)) return; + if (agent == null || string.IsNullOrWhiteSpace(agent.Id)) return; switch (field) { @@ -57,7 +56,10 @@ public partial class MongoRepository UpdateAgentLlmConfig(agent.Id, agent.LlmConfig); break; case AgentField.Utility: - UpdateAgentUtilities(agent.Id, agent.Utilities); + UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities); + break; + case AgentField.MaxMessageCount: + UpdateAgentMaxMessageCount(agent.Id, agent.MaxMessageCount); break; case AgentField.All: UpdateAgentAllFields(agent); @@ -159,10 +161,8 @@ public partial class MongoRepository private void UpdateAgentInstructions(string agentId, string instruction, List? channelInstructions) { - if (string.IsNullOrWhiteSpace(agentId)) return; - var instructionElements = channelInstructions?.Select(x => ChannelInstructionMongoElement.ToMongoElement(x))? - .ToList() ?? new List(); + .ToList() ?? []; var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update @@ -201,7 +201,7 @@ public partial class MongoRepository private void UpdateAgentResponses(string agentId, List responses) { - if (responses == null) return; + if (responses == null || string.IsNullOrWhiteSpace(agentId)) return; var responsesToUpdate = responses.Select(r => AgentResponseMongoElement.ToMongoElement(r)).ToList(); var filter = Builders.Filter.Eq(x => x.Id, agentId); @@ -224,13 +224,16 @@ public partial class MongoRepository _dc.Agents.UpdateOne(filter, update); } - private void UpdateAgentUtilities(string agentId, List utilities) + private void UpdateAgentUtilities(string agentId, bool mergeUtility, List utilities) { if (utilities == null) return; + var elements = utilities?.Select(x => AgentUtilityMongoElement.ToMongoElement(x))?.ToList() ?? []; + var filter = Builders.Filter.Eq(x => x.Id, agentId); var update = Builders.Update - .Set(x => x.Utilities, utilities) + .Set(x => x.MergeUtility, mergeUtility) + .Set(x => x.Utilities, elements) .Set(x => x.UpdatedTime, DateTime.UtcNow); _dc.Agents.UpdateOne(filter, update); @@ -247,6 +250,16 @@ public partial class MongoRepository _dc.Agents.UpdateOne(filter, update); } + private void UpdateAgentMaxMessageCount(string agentId, int? maxMessageCount) + { + var filter = Builders.Filter.Eq(x => x.Id, agentId); + var update = Builders.Update + .Set(x => x.MaxMessageCount, maxMessageCount) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.Agents.UpdateOne(filter, update); + } + private void UpdateAgentAllFields(Agent agent) { var filter = Builders.Filter.Eq(x => x.Id, agent.Id); @@ -254,7 +267,9 @@ public partial class MongoRepository .Set(x => x.Name, agent.Name) .Set(x => x.Description, agent.Description) .Set(x => x.Disabled, agent.Disabled) + .Set(x => x.MergeUtility, agent.MergeUtility) .Set(x => x.Type, agent.Type) + .Set(x => x.MaxMessageCount, agent.MaxMessageCount) .Set(x => x.Profiles, agent.Profiles) .Set(x => x.RoutingRules, agent.RoutingRules.Select(r => RoutingRuleMongoElement.ToMongoElement(r)).ToList()) .Set(x => x.Instruction, agent.Instruction) @@ -263,7 +278,7 @@ public partial class MongoRepository .Set(x => x.Functions, agent.Functions.Select(f => FunctionDefMongoElement.ToMongoElement(f)).ToList()) .Set(x => x.Responses, agent.Responses.Select(r => AgentResponseMongoElement.ToMongoElement(r)).ToList()) .Set(x => x.Samples, agent.Samples) - .Set(x => x.Utilities, agent.Utilities) + .Set(x => x.Utilities, agent.Utilities.Select(u => AgentUtilityMongoElement.ToMongoElement(u)).ToList()) .Set(x => x.LlmConfig, AgentLlmConfigMongoElement.ToMongoElement(agent.LlmConfig)) .Set(x => x.IsPublic, agent.IsPublic) .Set(x => x.UpdatedTime, DateTime.UtcNow); @@ -274,7 +289,7 @@ public partial class MongoRepository #endregion - public Agent? GetAgent(string agentId) + public Agent? GetAgent(string agentId, bool basicsOnly = false) { var agent = _dc.Agents.AsQueryable().FirstOrDefault(x => x.Id == agentId); if (agent == null) return null; @@ -406,28 +421,20 @@ public partial class MongoRepository IconUrl = x.IconUrl, Description = x.Description, Instruction = x.Instruction, - ChannelInstructions = x.ChannelInstructions? - .Select(i => ChannelInstructionMongoElement.ToMongoElement(i))? - .ToList() ?? new List(), - Templates = x.Templates? - .Select(t => AgentTemplateMongoElement.ToMongoElement(t))? - .ToList() ?? new List(), - Functions = x.Functions? - .Select(f => FunctionDefMongoElement.ToMongoElement(f))? - .ToList() ?? new List(), - Responses = x.Responses? - .Select(r => AgentResponseMongoElement.ToMongoElement(r))? - .ToList() ?? new List(), + ChannelInstructions = x.ChannelInstructions?.Select(i => ChannelInstructionMongoElement.ToMongoElement(i))?.ToList() ?? [], + Templates = x.Templates?.Select(t => AgentTemplateMongoElement.ToMongoElement(t))?.ToList() ?? [], + Functions = x.Functions?.Select(f => FunctionDefMongoElement.ToMongoElement(f))?.ToList() ?? [], + Responses = x.Responses?.Select(r => AgentResponseMongoElement.ToMongoElement(r))?.ToList() ?? [], Samples = x.Samples ?? new List(), - Utilities = x.Utilities ?? new List(), + Utilities = x.Utilities?.Select(u => AgentUtilityMongoElement.ToMongoElement(u))?.ToList() ?? [], IsPublic = x.IsPublic, Type = x.Type, InheritAgentId = x.InheritAgentId, Disabled = x.Disabled, + MergeUtility = x.MergeUtility, + MaxMessageCount = x.MaxMessageCount, Profiles = x.Profiles, - RoutingRules = x.RoutingRules? - .Select(r => RoutingRuleMongoElement.ToMongoElement(r))? - .ToList() ?? new List(), + RoutingRules = x.RoutingRules?.Select(r => RoutingRuleMongoElement.ToMongoElement(r))?.ToList() ?? [], LlmConfig = AgentLlmConfigMongoElement.ToMongoElement(x.LlmConfig), CreatedTime = x.CreatedDateTime, UpdatedTime = x.UpdatedDateTime @@ -505,29 +512,21 @@ public partial class MongoRepository IconUrl = agentDoc.IconUrl, Description = agentDoc.Description, Instruction = agentDoc.Instruction, - ChannelInstructions = !agentDoc.ChannelInstructions.IsNullOrEmpty() ? agentDoc.ChannelInstructions - .Select(i => ChannelInstructionMongoElement.ToDomainElement(i)) - .ToList() : new List(), - Templates = !agentDoc.Templates.IsNullOrEmpty() ? agentDoc.Templates - .Select(t => AgentTemplateMongoElement.ToDomainElement(t)) - .ToList() : new List(), - Functions = !agentDoc.Functions.IsNullOrEmpty() ? agentDoc.Functions - .Select(f => FunctionDefMongoElement.ToDomainElement(f)) - .ToList() : new List(), - Responses = !agentDoc.Responses.IsNullOrEmpty() ? agentDoc.Responses - .Select(r => AgentResponseMongoElement.ToDomainElement(r)) - .ToList() : new List(), - RoutingRules = !agentDoc.RoutingRules.IsNullOrEmpty() ? agentDoc.RoutingRules - .Select(r => RoutingRuleMongoElement.ToDomainElement(agentDoc.Id, agentDoc.Name, r)) - .ToList() : new List(), + ChannelInstructions = agentDoc.ChannelInstructions?.Select(i => ChannelInstructionMongoElement.ToDomainElement(i))?.ToList() ?? [], + Templates = agentDoc.Templates?.Select(t => AgentTemplateMongoElement.ToDomainElement(t))?.ToList() ?? [], + Functions = agentDoc.Functions?.Select(f => FunctionDefMongoElement.ToDomainElement(f)).ToList() ?? [], + Responses = agentDoc.Responses?.Select(r => AgentResponseMongoElement.ToDomainElement(r))?.ToList() ?? [], + RoutingRules = agentDoc.RoutingRules?.Select(r => RoutingRuleMongoElement.ToDomainElement(agentDoc.Id, agentDoc.Name, r))?.ToList() ?? [], LlmConfig = AgentLlmConfigMongoElement.ToDomainElement(agentDoc.LlmConfig), - Samples = agentDoc.Samples ?? new List(), - Utilities = agentDoc.Utilities ?? new List(), + Samples = agentDoc.Samples ?? [], + Utilities = agentDoc.Utilities?.Select(u => AgentUtilityMongoElement.ToDomainElement(u))?.ToList() ?? [], IsPublic = agentDoc.IsPublic, Disabled = agentDoc.Disabled, + MergeUtility = agentDoc.MergeUtility, Type = agentDoc.Type, InheritAgentId = agentDoc.InheritAgentId, Profiles = agentDoc.Profiles, + MaxMessageCount = agentDoc.MaxMessageCount }; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index ccbfc1af..468f467a 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -56,6 +56,7 @@ public partial class MongoRepository var filterPromptLog = Builders.Filter.In(x => x.ConversationId, conversationIds); var filterContentLog = Builders.Filter.In(x => x.ConversationId, conversationIds); var filterStateLog = Builders.Filter.In(x => x.ConversationId, conversationIds); + var conbTabItems = Builders.Filter.In(x => x.ConversationId, conversationIds); var exeLogDeleted = _dc.ExectionLogs.DeleteMany(filterExeLog); var promptLogDeleted = _dc.LlmCompletionLogs.DeleteMany(filterPromptLog); @@ -63,11 +64,13 @@ public partial class MongoRepository var stateLogDeleted = _dc.StateLogs.DeleteMany(filterStateLog); var statesDeleted = _dc.ConversationStates.DeleteMany(filterSates); var dialogDeleted = _dc.ConversationDialogs.DeleteMany(filterDialog); + var cronDeleted = _dc.CrontabItems.DeleteMany(conbTabItems); var convDeleted = _dc.Conversations.DeleteMany(filterConv); return convDeleted.DeletedCount > 0 || dialogDeleted.DeletedCount > 0 || statesDeleted.DeletedCount > 0 || exeLogDeleted.DeletedCount > 0 || promptLogDeleted.DeletedCount > 0 - || contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0; + || contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0 + || convDeleted.DeletedCount > 0; } [SideCar] @@ -111,6 +114,17 @@ public partial class MongoRepository _dc.Conversations.UpdateOne(filterConv, updateConv); } + public void UpdateConversationTitleAlias(string conversationId, string titleAlias) + { + if (string.IsNullOrEmpty(conversationId)) return; + + var filterConv = Builders.Filter.Eq(x => x.Id, conversationId); + var updateConv = Builders.Update + .Set(x => x.UpdatedTime, DateTime.UtcNow) + .Set(x => x.TitleAlias, titleAlias); + + _dc.Conversations.UpdateOne(filterConv, updateConv); + } public bool UpdateConversationTags(string conversationId, List tags) { @@ -298,6 +312,10 @@ public partial class MongoRepository { convFilters.Add(convBuilder.Regex(x => x.Title, new BsonRegularExpression(filter.Title, "i"))); } + if (!string.IsNullOrEmpty(filter?.TitleAlias)) + { + convFilters.Add(convBuilder.Regex(x => x.Title, new BsonRegularExpression(filter.TitleAlias, "i"))); + } if (!string.IsNullOrEmpty(filter?.AgentId)) { convFilters.Add(convBuilder.Eq(x => x.AgentId, filter.AgentId)); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs new file mode 100644 index 00000000..a40fc827 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Crontab.cs @@ -0,0 +1,87 @@ +using BotSharp.Abstraction.Crontab.Models; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.MongoStorage.Repository; + +public partial class MongoRepository +{ + public bool UpsertCrontabItem(CrontabItem item) + { + if (item == null || string.IsNullOrWhiteSpace(item.ConversationId)) + { + return false; + } + + try + { + var cronDoc = CrontabItemDocument.ToMongoModel(item); + cronDoc.Id = Guid.NewGuid().ToString(); + + var filter = Builders.Filter.Eq(x => x.ConversationId, item.ConversationId); + var result = _dc.CrontabItems.ReplaceOne(filter, cronDoc, new ReplaceOptions + { + IsUpsert = true + }); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Error when saving crontab item: {ex.Message}\r\n{ex.InnerException}"); + return false; + } + } + + + public bool DeleteCrontabItem(string conversationId) + { + if (string.IsNullOrWhiteSpace(conversationId)) + { + return false; + } + + var filter = Builders.Filter.Eq(x => x.ConversationId, conversationId); + var result = _dc.CrontabItems.DeleteMany(filter); + return result.DeletedCount > 0; + } + + + public PagedItems GetCrontabItems(CrontabItemFilter filter) + { + if (filter == null) + { + filter = CrontabItemFilter.Empty(); + } + + var cronBuilder = Builders.Filter; + var cronFilters = new List>() { cronBuilder.Empty }; + + // Filter cron + if (filter?.AgentIds != null) + { + cronFilters.Add(cronBuilder.In(x => x.AgentId, filter.AgentIds)); + } + if (filter?.ConversationIds != null) + { + cronFilters.Add(cronBuilder.In(x => x.ConversationId, filter.ConversationIds)); + } + if (filter?.UserIds != null) + { + cronFilters.Add(cronBuilder.In(x => x.UserId, filter.UserIds)); + } + + // Sort and paginate + var filterDef = cronBuilder.And(cronFilters); + var sortDef = Builders.Sort.Descending(x => x.CreatedTime); + + var cronDocs = _dc.CrontabItems.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); + var count = _dc.CrontabItems.CountDocuments(filterDef); + + var crontabItems = cronDocs.Select(x => CrontabItemDocument.ToDomainModel(x)).ToList(); + + return new PagedItems + { + Items = crontabItems, + Count = (int)count + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index e999e034..3c76e21d 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -13,7 +13,7 @@ public partial class MongoRepository return user != null ? user.ToUser() : null; } - public User? GetUserByPhone(string phone, string role = null, string regionCode = "CN") + public User? GetUserByPhone(string phone, string type = UserType.Client, string regionCode = "CN") { string phoneSecond = string.Empty; // if phone number length is less than 4, return null @@ -22,11 +22,18 @@ public partial class MongoRepository return null; } - phoneSecond = phone.StartsWith("+86") ? phone.Replace("+86", "") : $"+86{phone}"; + if (regionCode == "CN") + { + phoneSecond = (phone ?? "").StartsWith("+86") ? (phone ?? "").Replace("+86", "") : ($"+86{phone ?? ""}"); + } + else + { + phoneSecond = (phone ?? "").Substring(regionCode == "US" ? 2 : 3); + } - var user = _dc.Users.AsQueryable().FirstOrDefault(x => (x.Phone == phone || x.Phone == phoneSecond) && x.Type != UserType.Affiliate - && (x.RegionCode == regionCode || string.IsNullOrWhiteSpace(x.RegionCode)) - && (role == "admin" ? x.Role == "admin" || x.Role == "root" : true)); + var user = _dc.Users.AsQueryable().FirstOrDefault(x => (x.Phone == phone || x.Phone == phoneSecond) + && (x.RegionCode == regionCode || string.IsNullOrWhiteSpace(x.RegionCode)) + && (x.Type == type)); return user != null ? user.ToUser() : null; } @@ -245,7 +252,7 @@ public partial class MongoRepository user.AgentActions = agentActions; return user; } - + var agentIds = userAgents.Select(x => x.AgentId)?.Distinct().ToList(); if (!agentIds.IsNullOrEmpty()) { @@ -317,4 +324,51 @@ public partial class MongoRepository return true; } + + public void AddDashboardConversation(string userId, string conversationId) + { + var user = _dc.Users.AsQueryable() + .FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId)); + if (user == null) return; + var curDash = user.Dashboard ?? new Dashboard(); + curDash.ConversationList.Add(new DashboardConversation + { + Id = Guid.NewGuid().ToString(), + ConversationId = conversationId + }); + + var filter = Builders.Filter.Eq(x => x.Id, userId); + var update = Builders.Update.Set(x => x.Dashboard, curDash) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + } + + public void RemoveDashboardConversation(string userId, string conversationId) + { + var user = _dc.Users.AsQueryable() + .FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId)); + if (user == null || user.Dashboard == null || user.Dashboard.ConversationList.IsNullOrEmpty()) return; + var curDash = user.Dashboard; + var unpinConv = user.Dashboard.ConversationList.FirstOrDefault( + x => string.Equals(x.ConversationId, conversationId, StringComparison.OrdinalIgnoreCase)); + if (unpinConv == null) return; + curDash.ConversationList.Remove(unpinConv); + + var filter = Builders.Filter.Eq(x => x.Id, userId); + var update = Builders.Update.Set(x => x.Dashboard, curDash) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + } + + public void UpdateDashboardConversation(string userId, DashboardConversation dashConv) + { + var user = _dc.Users.AsQueryable() + .FirstOrDefault(x => x.Id == userId || (x.ExternalId != null && x.ExternalId == userId)); + if (user == null || user.Dashboard == null || user.Dashboard.ConversationList.IsNullOrEmpty()) return; + var curIdx = user.Dashboard.ConversationList.ToList().FindIndex( + x => string.Equals(x.ConversationId, dashConv.ConversationId, StringComparison.OrdinalIgnoreCase)); + if (curIdx < 0) return; + + var filter = Builders.Filter.Eq(x => x.Id, userId); + var update = Builders.Update.Set(x => x.Dashboard.ConversationList[curIdx], dashConv) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + } } diff --git a/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj b/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj index 72c4e710..886964b5 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj +++ b/src/Plugins/BotSharp.Plugin.Planner/BotSharp.Plugin.Planner.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -19,10 +19,11 @@ - - - - + + + + + @@ -53,13 +54,49 @@ PreserveNewest - + PreserveNewest - + PreserveNewest - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.Planner/Enums/PlannerAgentId.cs b/src/Plugins/BotSharp.Plugin.Planner/Enums/PlannerAgentId.cs new file mode 100644 index 00000000..ce603861 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/Enums/PlannerAgentId.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Plugin.Planner.Enums; + +public class PlannerAgentId +{ + public const string TwoStagePlanner = "282a7128-69a1-44b0-878c-a9159b88f3b9"; + public const string SequentialPlanner = "3e75e818-a139-48a8-9e22-4662548c13a3"; + public const string SqlPlanner = "da7aad2c-8112-48a2-ab7b-1f87da524741"; +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.Planner/Enums/UtilityName.cs index 8a3ef6e3..7594fae0 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Enums/UtilityName.cs @@ -2,5 +2,5 @@ namespace BotSharp.Plugin.Planner.Enums; public class UtilityName { - public const string TwoStagePlanner = "two-stage-planner"; + public const string TwoStagePlanner = "planner.two-stage-planner"; } diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs index 84378158..fa1b7166 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/PrimaryStagePlanFn.cs @@ -49,8 +49,8 @@ public class PrimaryStagePlanFn : IFunctionCallback var prompt = await GetFirstStagePlanPrompt(message, task.Requirements, knowledges); var plannerAgent = new Agent { - Id = BuiltInAgentId.Planner, - Name = "FirstStagePlanner", + Id = message.CurrentAgentId, + Name = Name, Instruction = prompt, TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig @@ -70,7 +70,7 @@ public class PrimaryStagePlanFn : IFunctionCallback var render = _services.GetRequiredService(); var knowledgeHooks = _services.GetServices(); - var agent = await agentService.GetAgent(BuiltInAgentId.Planner); + var agent = await agentService.GetAgent(PlannerAgentId.TwoStagePlanner); var template = agent.Templates.FirstOrDefault(x => x.Name == "two_stage.1st.plan")?.Content ?? string.Empty; var responseFormat = JsonSerializer.Serialize(new FirstStagePlan{}); diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs index be780f51..5044139c 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/SecondaryStagePlanFn.cs @@ -53,8 +53,8 @@ public class SecondaryStagePlanFn : IFunctionCallback var plannerAgent = new Agent { - Id = BuiltInAgentId.Planner, - Name = "SecondStagePlanner", + Id = PlannerAgentId.TwoStagePlanner, + Name = Name, Instruction = prompt, TemplateDict = new Dictionary(), LlmConfig = currentAgent.LlmConfig diff --git a/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs b/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs index 2f5a1708..b89bcc35 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Functions/SummaryPlanFn.cs @@ -1,8 +1,5 @@ -using BotSharp.Abstraction.Planning; using BotSharp.Plugin.Planner.TwoStaging; using BotSharp.Plugin.Planner.TwoStaging.Models; -using static System.Net.Mime.MediaTypeNames; -using System.Text.RegularExpressions; namespace BotSharp.Plugin.Planner.Functions; @@ -62,8 +59,8 @@ public class SummaryPlanFn : IFunctionCallback var plannerAgent = new Agent { - Id = BuiltInAgentId.Planner, - Name = "SummaryPlanner", + Id = PlannerAgentId.TwoStagePlanner, + Name = Name, Instruction = prompt, LlmConfig = currentAgent.LlmConfig }; @@ -71,13 +68,6 @@ public class SummaryPlanFn : IFunctionCallback var summary = await GetAiResponse(plannerAgent); message.Content = summary.Content; - // Validate the sql result - var args = JsonSerializer.Deserialize(message.FunctionArgs); - if (args.IsSqlTemplate == false) - { - await fn.InvokeFunction("validate_sql", message); - } - await HookEmitter.Emit(_services, async hook => await hook.OnPlanningCompleted(nameof(TwoStageTaskPlanner), message) ); @@ -91,7 +81,7 @@ public class SummaryPlanFn : IFunctionCallback var render = _services.GetRequiredService(); var knowledgeHooks = _services.GetServices(); - var agent = await agentService.GetAgent(BuiltInAgentId.Planner); + var agent = await agentService.GetAgent(PlannerAgentId.TwoStagePlanner); var template = agent.Templates.FirstOrDefault(x => x.Name == "two_stage.summarize")?.Content ?? string.Empty; var additionalRequirements = new List(); diff --git a/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs b/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs index 503de3d5..8c6f2635 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerAgentHook.cs @@ -2,7 +2,7 @@ namespace BotSharp.Plugin.Planner.Hooks; public class PlannerAgentHook : AgentHookBase { - public override string SelfId => BuiltInAgentId.Planner; + public override string SelfId => PlannerAgentId.TwoStagePlanner; public PlannerAgentHook(IServiceProvider services, AgentSettings settings) : base(services, settings) @@ -19,7 +19,7 @@ public class PlannerAgentHook : AgentHookBase { var k = hook.GetGlobalKnowledges(new RoleDialogModel(AgentRole.User, template) { - CurrentAgentId = BuiltInAgentId.Planner + CurrentAgentId = PlannerAgentId.TwoStagePlanner }).Result; Knowledges.AddRange(k); } @@ -27,79 +27,4 @@ public class PlannerAgentHook : AgentHookBase return true; } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.TwoStagePlanner); - - if (isConvMode && isEnabled) - { - var (prompt, fn) = GetPromptAndFunction("plan_primary_stage"); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - - (prompt, fn) = GetPromptAndFunction("plan_secondary_stage"); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - - (prompt, fn) = GetPromptAndFunction("plan_summary"); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, FunctionDef?) GetPromptAndFunction(string functionName) - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{functionName}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(functionName)); - return (prompt, loadAttachmentFn); - } } diff --git a/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerUtilityHook.cs index 08d3f8f5..b10d02ac 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Hooks/PlannerUtilityHook.cs @@ -2,8 +2,27 @@ namespace BotSharp.Plugin.Planner.Hooks; public class PlannerUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private const string PRIMARY_STAGE_FN = "plan_primary_stage"; + private const string SECONDARY_STAGE_FN = "plan_secondary_stage"; + private const string SUMMARY_FN = "plan_summary"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.TwoStagePlanner); + var utility = new AgentUtility + { + Name = UtilityName.TwoStagePlanner, + Functions = [ + new(PRIMARY_STAGE_FN), + new(SECONDARY_STAGE_FN), + new(SUMMARY_FN) + ], + Templates = [ + new($"{PRIMARY_STAGE_FN}.fn"), + new($"{SECONDARY_STAGE_FN}.fn"), + new($"{SUMMARY_FN}.fn") + ] + }; + + utilities.Add(utility); } } diff --git a/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs b/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs index aba02745..9ece599f 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/PlannerPlugin.cs @@ -1,4 +1,5 @@ -using BotSharp.Abstraction.Routing.Planning; +using BotSharp.Plugin.Planner.Sequential; +using BotSharp.Plugin.Planner.SqlGeneration; using BotSharp.Plugin.Planner.TwoStaging; namespace BotSharp.Plugin.Planner; @@ -13,11 +14,18 @@ public class PlannerPlugin : IBotSharpPlugin public string Description => "Provide AI with different planning approaches to improve AI's ability to solve complex problems."; public string IconUrl => "https://e7.pngegg.com/pngimages/775/350/png-clipart-action-plan-computer-icons-plan-miscellaneous-text-thumbnail.png"; - public string[] AgentIds => [ BuiltInAgentId.Planner ]; + public string[] AgentIds => + [ + PlannerAgentId.TwoStagePlanner, + PlannerAgentId.SequentialPlanner, + PlannerAgentId.SqlPlanner + ]; public void RegisterDI(IServiceCollection services, IConfiguration config) { - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs similarity index 80% rename from src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs rename to src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs index f6b05375..043c7380 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Sequential/SequentialPlanner.cs @@ -1,20 +1,36 @@ -using BotSharp.Abstraction.MLTasks; -using BotSharp.Abstraction.Routing.Models; -using BotSharp.Abstraction.Routing.Planning; -using BotSharp.Abstraction.Templating; +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. -namespace BotSharp.Core.Routing.Planning; + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -public class SequentialPlanner : IRoutingPlaner + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ + +namespace BotSharp.Plugin.Planner.Sequential; + +/// +/// A Sequential Planner for a large language model (LLM) is a framework or methodology to execute some list tasks in a predefined order by the user, +/// LLM can follow to produce an organized and coherent output. +/// Sequential Planners are useful for tasks that involve multiple stages of reasoning, information retrieval, or interdependent subtasks. +/// +public class SequentialPlanner : ITaskPlanner { private readonly IServiceProvider _services; private readonly ILogger _logger; - + public string Name => "Sequential-Planner"; public bool HideDialogContext => true; public int MaxLoopCount => 100; private FunctionCallFromLlm _lastInst; - public SequentialPlanner(IServiceProvider services, ILogger logger) + public SequentialPlanner(IServiceProvider services, ILogger logger) { _services = services; _logger = logger; @@ -154,7 +170,7 @@ public class SequentialPlanner : IRoutingPlaner private string GetNextStepPrompt(Agent router) { - var template = router.Templates.First(x => x.Name == "planner_prompt.sequential").Content; + var template = router.Templates.First(x => x.Name == "reasoner.sequential").Content; var render = _services.GetRequiredService(); return render.Render(template, new Dictionary @@ -169,11 +185,11 @@ public class SequentialPlanner : IRoutingPlaner var inst = new DecomposedStep(); var llmProviderService = _services.GetRequiredService(); - var model = llmProviderService.GetProviderModel("azure-openai", "gpt-4"); + var model = llmProviderService.GetProviderModel("openai", "gpt-4o"); // chat completion var completion = CompletionProvider.GetChatCompletion(_services, - provider: "azure-openai", + provider: "openai", model: model.Name); int retryCount = 0; @@ -208,16 +224,11 @@ public class SequentialPlanner : IRoutingPlaner private string GetDecomposeTaskPrompt(Agent router) { - var template = router.Templates.First(x => x.Name == "planner_prompt.sequential.get_remaining_task").Content; + var template = router.Templates.First(x => x.Name == "reasoner.sequential.get_remaining_task").Content; var render = _services.GetRequiredService(); return render.Render(template, new Dictionary { }); } - - public Task GetNextInstruction(Agent router, string messageId) - { - throw new NotImplementedException(); - } } diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlGenerationFn.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlGenerationFn.cs new file mode 100644 index 00000000..e89f533a --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlGenerationFn.cs @@ -0,0 +1,127 @@ +using BotSharp.Plugin.Planner.TwoStaging; +using BotSharp.Plugin.Planner.TwoStaging.Models; + +namespace BotSharp.Plugin.Planner.Functions; + +public class SqlGenerationFn : IFunctionCallback +{ + public string Name => "sql_generation"; + public string Indication => "Organizing and summarizing the final SQL statements."; + + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public SqlGenerationFn( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Execute(RoleDialogModel message) + { + var fn = _services.GetRequiredService(); + var agentService = _services.GetRequiredService(); + var states = _services.GetRequiredService(); + + states.SetState("max_tokens", "4096"); + var currentAgent = await agentService.LoadAgent(message.CurrentAgentId); + var taskRequirement = states.GetState("requirement_detail"); + + // Get table names + var steps = states.GetState("planning_result").JsonArrayContent(); + var allTables = new List(); + var ddlStatements = string.Empty; + var domainKnowledge = states.GetState("planning_result"); + domainKnowledge += "\r\n" + states.GetState("domain_knowledges"); + var dictionaryItems = states.GetState("dictionary_items"); + var excelImportResult = states.GetState("excel_import_result"); + + foreach (var step in steps) + { + allTables.AddRange(step.Tables); + } + var distinctTables = allTables.Distinct().ToList(); + + var msgCopy = RoleDialogModel.From(message); + msgCopy.FunctionArgs = JsonSerializer.Serialize(new + { + tables = distinctTables, + }); + await fn.InvokeFunction("sql_table_definition", msgCopy); + ddlStatements += "\r\n" + msgCopy.Content; + states.SetState("table_ddls", ddlStatements); + + // Summarize and generate query + var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, domainKnowledge, dictionaryItems, ddlStatements, excelImportResult); + _logger.LogInformation($"Summary plan prompt:\r\n{prompt}"); + + var plannerAgent = new Agent + { + Id = PlannerAgentId.TwoStagePlanner, + Name = Name, + Instruction = prompt, + LlmConfig = currentAgent.LlmConfig + }; + + var summary = await GetAiResponse(plannerAgent); + message.Content = summary.Content; + + /*await HookEmitter.Emit(_services, async hook => + await hook.OnPlanningCompleted(nameof(TwoStageTaskPlanner), message) + );*/ + + return true; + } + + private async Task GetSummaryPlanPrompt(RoleDialogModel message, string taskDescription, string domainKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult) + { + var agentService = _services.GetRequiredService(); + var render = _services.GetRequiredService(); + var knowledgeHooks = _services.GetServices(); + + var agent = await agentService.GetAgent(PlannerAgentId.TwoStagePlanner); + var template = agent.Templates.FirstOrDefault(x => x.Name == "two_stage.summarize")?.Content ?? string.Empty; + + var additionalRequirements = new List(); + await HookEmitter.Emit(_services, async x => + { + var requirement = await x.GetSummaryAdditionalRequirements(nameof(TwoStageTaskPlanner), message); + additionalRequirements.Add(requirement); + }); + + var globalKnowledges = new List(); + foreach (var hook in knowledgeHooks) + { + var k = await hook.GetGlobalKnowledges(message); + globalKnowledges.AddRange(k); + } + + return render.Render(template, new Dictionary + { + { "task_description", taskDescription }, + { "summary_requirements", string.Join("\r\n", additionalRequirements) }, + { "global_knowledges", globalKnowledges }, + { "domain_knowledges", domainKnowledge }, + { "dictionary_items", dictionaryItems }, + { "table_structure", ddlStatement }, + { "excel_import_result", excelImportResult } + }); + } + private async Task GetAiResponse(Agent plannerAgent) + { + var conv = _services.GetRequiredService(); + var wholeDialogs = conv.GetDialogHistory(); + + // Append text + wholeDialogs.Last().Content += "\n\nIf the table structure didn't mention auto incremental, the data field id needs to insert id manually and you need to use max(id).\nFor example, you should use SET @id = select max(id) from table;"; + wholeDialogs.Last().Content += "\n\nTry if you can generate a single query to fulfill the needs."; + + var completion = CompletionProvider.GetChatCompletion(_services, + provider: plannerAgent.LlmConfig.Provider, + model: plannerAgent.LlmConfig.Model); + + return await completion.GetChatCompletions(plannerAgent, wholeDialogs); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlReviewFn.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlReviewFn.cs new file mode 100644 index 00000000..7c14d5f4 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Functions/SqlReviewFn.cs @@ -0,0 +1,38 @@ +using BotSharp.Plugin.Planner.SqlGeneration.Models; +using BotSharp.Plugin.Planner.TwoStaging; +using BotSharp.Plugin.Planner.TwoStaging.Models; + +namespace BotSharp.Plugin.Planner.SqlGeneration.Functions; + +public class SqlReviewFn : IFunctionCallback +{ + public string Name => "sql_review"; + public string Indication => "Currently reviewing SQL statement"; + + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public SqlReviewFn( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + if (!message.Content.StartsWith("```sql")) + { + message.Content = $"```sql\r\n{args.SqlStatement}\r\n```"; + } + if (args != null && !args.IsSqlTemplate && args.ContainsSqlStatements) + { + await HookEmitter.Emit(_services, async hook => + await hook.OnSourceCodeGenerated(nameof(TwoStageTaskPlanner), message, "sql") + ); + } + return true; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/FirstStagePlan.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/FirstStagePlan.cs new file mode 100644 index 00000000..17ca9f0e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/FirstStagePlan.cs @@ -0,0 +1,39 @@ +namespace BotSharp.Plugin.Planner.SqlGeneration.Models; + +public class FirstStagePlan +{ + [JsonPropertyName("task_detail")] + public string Task { get; set; } = ""; + + //[JsonPropertyName("reason")] + //public string Reason { get; set; } = ""; + + [JsonPropertyName("step")] + public int Step { get; set; } = -1; + + [JsonPropertyName("need_breakdown_task")] + public bool NeedAdditionalInformation { get; set; } = false; + + [JsonPropertyName("need_lookup_dictionary")] + public bool NeedLookupDictionary { get; set; } = false; + + [JsonPropertyName("related_tables")] + public string[] Tables { get; set; } = []; + + [JsonPropertyName("has_found_relevant_knowledge")] + public bool HasFoundRelevantKnowledge { get; set; } = false; + + //[JsonPropertyName("related_urls")] + //public string[] Urls { get; set; } = []; + + //[JsonPropertyName("input_args")] + //public JsonDocument[] Parameters { get; set; } = []; + + //[JsonPropertyName("output_results")] + //public string[] Results { get; set; } = []; + + public override string ToString() + { + return $"STEP {Step}: {Task}"; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/PrimaryRequirementRequest.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/PrimaryRequirementRequest.cs new file mode 100644 index 00000000..6a15faba --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/PrimaryRequirementRequest.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.Planner.SqlGeneration.Models; + +public class PrimaryRequirementRequest +{ + [JsonPropertyName("requirement_detail")] + public string Requirements { get; set; } = null!; + + [JsonPropertyName("questions")] + public string[] Questions { get; set; } = []; + + [JsonPropertyName("norm_questions")] + public string[] NormQuestions { get; set; } = []; +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SecondStagePlan.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SecondStagePlan.cs new file mode 100644 index 00000000..49f78f23 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SecondStagePlan.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Plugin.Planner.SqlGeneration.Models; + +public class SecondStagePlan +{ + [JsonPropertyName("related_tables")] + public string[] Tables { get; set; } = []; + + [JsonPropertyName("need_lookup_dictionary")] + public bool NeedLookupDictionary { get; set; } = false; + + [JsonPropertyName("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("input_args")] + public JsonDocument[] Parameters { get; set; } = []; + + [JsonPropertyName("output_results")] + public string[] Results { get; set; } = []; +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SecondaryBreakdownTask.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SecondaryBreakdownTask.cs new file mode 100644 index 00000000..671c6353 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SecondaryBreakdownTask.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.Planner.SqlGeneration.Models; + +public class SecondaryBreakdownTask +{ + [JsonPropertyName("task_description")] + public string TaskDescription { get; set; } = null!; + + [JsonPropertyName("solution_search_question")] + public string SolutionQuestion { get; set; } = null!; + + [JsonPropertyName("need_lookup_dictionary")] + public bool NeedLookupDictionary { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SqlReviewArgs.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SqlReviewArgs.cs new file mode 100644 index 00000000..29ec186f --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/Models/SqlReviewArgs.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Plugin.Planner.SqlGeneration.Models; + +public class SqlReviewArgs +{ + [JsonPropertyName("is_sql_template")] + public bool IsSqlTemplate { get; set; } = false; + + [JsonPropertyName("contains_sql_statements")] + public bool ContainsSqlStatements { get; set; } = false; + + [JsonPropertyName("sql_statement")] + public string SqlStatement { get; set; } = string.Empty; +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs new file mode 100644 index 00000000..52571578 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/SqlGeneration/SqlGenerationPlanner.cs @@ -0,0 +1,107 @@ +namespace BotSharp.Plugin.Planner.SqlGeneration; + +public class SqlGenerationPlanner : ITaskPlanner +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + public string Name => "SQL-Planner"; + public int MaxLoopCount => 10; + + public SqlGenerationPlanner(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task GetNextInstruction(Agent router, string messageId, List dialogs) + { + var inst = new FunctionCallFromLlm(); + var nextStepPrompt = await GetNextStepPrompt(router); + + // chat completion + var completion = CompletionProvider.GetChatCompletion(_services, + provider: router?.LlmConfig?.Provider, + model: router?.LlmConfig?.Model); + + // text completion + dialogs = new List + { + new RoleDialogModel(AgentRole.User, nextStepPrompt) + { + FunctionName = nameof(SqlGenerationPlanner), + MessageId = messageId + } + }; + var response = await completion.GetChatCompletions(router, dialogs); + inst = response.Content.JsonContent(); + + // Fix LLM malformed response + ReasonerHelper.FixMalformedResponse(_services, inst); + return inst; + } + + public List BeforeHandleContext(FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + var question = inst.Response; + + var taskAgentDialogs = new List + { + new RoleDialogModel(AgentRole.User, question) + { + MessageId = message.MessageId, + } + }; + + return taskAgentDialogs; + } + + public bool AfterHandleContext(List dialogs, List taskAgentDialogs) + { + dialogs.AddRange(taskAgentDialogs.Skip(1)); + + return true; + } + + public async Task AgentExecuting(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + // Set user content as Planner's question + message.FunctionName = inst.Function; + message.FunctionArgs = inst.Arguments == null ? "{}" : JsonSerializer.Serialize(inst.Arguments); + return true; + } + + public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, RoleDialogModel message, List dialogs) + { + var context = _services.GetRequiredService(); + + if (message.StopCompletion) + { + context.Empty(reason: $"Agent queue is cleared by {nameof(SqlGenerationPlanner)}"); + return false; + } + + if (dialogs.Last().Role == AgentRole.Assistant) + { + context.Empty(); + return false; + } + + var routing = _services.GetRequiredService(); + routing.Context.ResetRecursiveCounter(); + return true; + } + + private async Task GetNextStepPrompt(Agent router) + { + var agentService = _services.GetRequiredService(); + var planner = await agentService.LoadAgent(PlannerAgentId.TwoStagePlanner); + var template = planner.Templates.First(x => x.Name == "two_stage.next").Content; + var states = _services.GetRequiredService(); + var render = _services.GetRequiredService(); + return render.Render(template, new Dictionary + { + { StateConst.EXPECTED_ACTION_AGENT, states.GetState(StateConst.EXPECTED_ACTION_AGENT) }, + { StateConst.EXPECTED_GOAL_AGENT, states.GetState(StateConst.EXPECTED_GOAL_AGENT) } + }); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SummaryPlan.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SummaryPlan.cs index 459237a8..6b156c79 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SummaryPlan.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/Models/SummaryPlan.cs @@ -4,4 +4,7 @@ public class SummaryPlan { [JsonPropertyName("is_sql_template")] public bool IsSqlTemplate { get; set; } = false; + + [JsonPropertyName("contains_sql_statements")] + public bool ContainsSqlStatements { get; set; } = false; } diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs index 565115a5..6935bfbf 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -1,13 +1,38 @@ -using BotSharp.Abstraction.Infrastructures.Enums; -using BotSharp.Abstraction.Routing.Planning; -using BotSharp.Core.Routing.Planning; +/***************************************************************************** + Copyright 2024 Written by Haiping Chen. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +******************************************************************************/ namespace BotSharp.Plugin.Planner.TwoStaging; -public partial class TwoStageTaskPlanner : IRoutingPlaner +/// +/// The Two-Stage Planner approach in large language models(LLMs) is an effective strategy to handle complex tasks by breaking them into manageable steps. +/// This involves creating a high-level plan in the first stage and then drilling down into the details in the second stage. +/// +/// Primary stage: +/// The LLM generates a structured overview or roadmap for solving the problem. +/// The focus here is on creating a broad and logically organized set of actions or categories that guide the overall process. +/// +/// Secondary stage: +/// For each step or component in the high-level plan, the LLM elaborates with specific actions, tools, or techniques. +/// The focus shifts to operationalizing the roadmap. +/// +public partial class TwoStageTaskPlanner : ITaskPlanner { private readonly IServiceProvider _services; private readonly ILogger _logger; + public string Name => "Two-Stage-Planner"; public int MaxLoopCount => 10; public TwoStageTaskPlanner(IServiceProvider services, ILogger logger) @@ -23,8 +48,8 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner // chat completion var completion = CompletionProvider.GetChatCompletion(_services, - provider: router?.LlmConfig?.Provider, - model: router?.LlmConfig?.Model); + provider: router?.LlmConfig?.Provider, + model: router?.LlmConfig?.Model); // text completion dialogs = new List @@ -36,12 +61,10 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner } }; var response = await completion.GetChatCompletions(router, dialogs); - inst = response.Content.JsonContent(); // Fix LLM malformed response - PlannerHelper.FixMalformedResponse(_services, inst); - + ReasonerHelper.FixMalformedResponse(_services, inst); return inst; } @@ -99,7 +122,7 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner private async Task GetNextStepPrompt(Agent router) { var agentService = _services.GetRequiredService(); - var planner = await agentService.LoadAgent(BuiltInAgentId.Planner); + var planner = await agentService.LoadAgent(PlannerAgentId.TwoStagePlanner); var template = planner.Templates.First(x => x.Name == "two_stage.next").Content; var states = _services.GetRequiredService(); var render = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.Planner/Using.cs b/src/Plugins/BotSharp.Plugin.Planner/Using.cs index 1e6737df..50db1596 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/Using.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/Using.cs @@ -18,19 +18,19 @@ global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Functions.Models; global using BotSharp.Abstraction.Repositories; global using BotSharp.Abstraction.Utilities; - global using BotSharp.Abstraction.Conversations.Models; global using BotSharp.Abstraction.Functions; global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.Templating; - global using BotSharp.Abstraction.Knowledges; global using BotSharp.Abstraction.Knowledges.Settings; -global using BotSharp.Abstraction.Knowledges.Enums; -global using BotSharp.Abstraction.VectorStorage.Models; -global using BotSharp.Abstraction.VectorStorage.Extensions; +global using BotSharp.Abstraction.Infrastructures.Enums; +global using BotSharp.Abstraction.Planning; +global using BotSharp.Abstraction.MLTasks; +global using BotSharp.Abstraction.Routing.Models; + +global using BotSharp.Core.Infrastructures; +global using BotSharp.Core.Routing.Reasoning; global using BotSharp.Plugin.Planner.Hooks; global using BotSharp.Plugin.Planner.Enums; - -global using BotSharp.Core.Infrastructures; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json index a90ef2ed..7f837847 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/agent.json @@ -1,18 +1,19 @@ { "id": "282a7128-69a1-44b0-878c-a9159b88f3b9", - "name": "Planner", - "description": "Plan feasible implementation steps for user task request", - "type": "task", + "name": "Two-Stage-Planner", + "description": "Plan feasible steps for complex user task request, including generating sql query", + "type": "planning", "createdDateTime": "2023-08-27T10:39:00Z", "updatedDateTime": "2023-08-27T14:39:00Z", "iconUrl": "https://e7.pngegg.com/pngimages/775/350/png-clipart-action-plan-computer-icons-plan-miscellaneous-text-thumbnail.png", "disabled": false, "isPublic": true, "profiles": [ "planning" ], - "utilities": [ "two-stage-planner", "sql-dictionary-lookup", "excel-handler" ], + "mergeUtility": true, + "utilities": [], "llmConfig": { "provider": "openai", - "model": "gpt-4o", + "model": "gpt-4o-2024-11-20", "max_recursion_depth": 10 } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/functions/plan_summary.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/functions/plan_summary.json index c13bfb25..17ec0639 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/functions/plan_summary.json +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/functions/plan_summary.json @@ -8,6 +8,10 @@ "type": "boolean", "description": "If user request is to generate sql template instead of actual sql statement." }, + "contains_sql_statements": { + "type": "boolean", + "description": "Set to true if the response contains sql statements." + }, "related_tables": { "type": "array", "description": "table name in planning steps", @@ -17,6 +21,6 @@ } } }, - "required": [ "related_tables", "is_sql_template" ] + "required": [ "related_tables", "is_sql_template", "contains_sql_statements" ] } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid index 89bcd49b..00da3cd6 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/instructions/instruction.liquid @@ -15,6 +15,7 @@ Don't run the planning process repeatedly if you have already got the result of Function verify_dictionary_term CAN'T generate INSERT SQL Statement. The table name must come from the relevant knowledge. has_found_relevant_knowledge must be true. Do not introduce your actions or intentions in any way. +Only extract the Data related request, ignore the other request, such as schedule job, make phone call etc. {% if global_knowledges != empty -%} ===== diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid index 190edf8c..306f4214 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.1st.plan.liquid @@ -15,7 +15,8 @@ Thinking process: 4. Output all the subtasks as much detail as possible in JSON: [{{ response_format }}] 5. You can NOT generate the final query before calling function plan_summary. -Note:If the task includes repeat steps,e.g.same steps for multiple elements, only generate a single detailed solution without repeating steps for each elements. You can add multiple items in the input and output args. +Note: +* If the task includes repeat steps,e.g.same steps for multiple elements, only generate a single detailed solution without repeating steps for each elements. {% if global_knowledges != empty -%} ===== diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid index c53abdab..d8f741a7 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/282a7128-69a1-44b0-878c-a9159b88f3b9/templates/two_stage.summarize.liquid @@ -1,4 +1,5 @@ -You are a planning summarizer. You will generate the final output in JSON format with short explanation based on the task description, knowledge and related table structure and relationship. +You are a planning summarizer. You will generate the final output in JSON format based on the task description, knowledge and related table structure and relationship. +Generate a simple business explaination of the quried data for the non tech audience. Requirements: {{ summary_requirements }} diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/3e75e818-a139-48a8-9e22-4662548c13a3/agent.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/3e75e818-a139-48a8-9e22-4662548c13a3/agent.json new file mode 100644 index 00000000..68a8d48b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/3e75e818-a139-48a8-9e22-4662548c13a3/agent.json @@ -0,0 +1,19 @@ +{ + "id": "3e75e818-a139-48a8-9e22-4662548c13a3", + "name": "Sequential-Planner", + "description": "Plan an execution steps for the user complex task, especially if the tasks are in a predefined order by the user", + "type": "planning", + "createdDateTime": "2023-08-27T10:39:00Z", + "updatedDateTime": "2023-08-27T14:39:00Z", + "iconUrl": "https://www.shutterstock.com/image-vector/sequential-process-glyph-icon-order-260nw-1015790182.jpg", + "disabled": false, + "isPublic": true, + "profiles": [ "planning" ], + "mergeUtility": true, + "utilities": [], + "llmConfig": { + "provider": "openai", + "model": "gpt-4o-2024-11-20", + "max_recursion_depth": 10 + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/3e75e818-a139-48a8-9e22-4662548c13a3/instructions/instruction.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/3e75e818-a139-48a8-9e22-4662548c13a3/instructions/instruction.liquid new file mode 100644 index 00000000..25e26106 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/3e75e818-a139-48a8-9e22-4662548c13a3/instructions/instruction.liquid @@ -0,0 +1,15 @@ +You are a Task Planner. you will breakdown user business requirements into excutable sub-steps. +Follow the instructions below to plan and execute tasks: + +1. Look at all the steps first, think carefully, and decide which step to do first. +2. Look at the description of each step, and use the existing functions and tools to plan the execution steps. + +{% if global_knowledges != empty -%} +===== +Global Knowledge: +Current date time is: {{ "now" | date: "%Y-%m-%d %H:%M" }} +{% for k in global_knowledges %} +{{ k }} +{% endfor %} +===== +{%- endif %} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_primary_stage.fn.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-planner-plan_primary_stage.fn.liquid similarity index 100% rename from src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_primary_stage.fn.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-planner-plan_primary_stage.fn.liquid diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_secondary_stage.fn.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-planner-plan_secondary_stage.fn.liquid similarity index 100% rename from src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_secondary_stage.fn.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-planner-plan_secondary_stage.fn.liquid diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_summary.fn.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-planner-plan_summary.fn.liquid similarity index 100% rename from src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/plan_summary.fn.liquid rename to src/Plugins/BotSharp.Plugin.Planner/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-planner-plan_summary.fn.liquid diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/agent.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/agent.json new file mode 100644 index 00000000..4d01b4e2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/agent.json @@ -0,0 +1,19 @@ +{ + "id": "da7aad2c-8112-48a2-ab7b-1f87da524741", + "name": "SQL-Planner", + "description": "Plan feasible steps for user task related to sql generation, generate sql statement and/or review the sql statement that can be derived from context", + "type": "planning", + "createdDateTime": "2023-08-27T10:39:00Z", + "updatedDateTime": "2023-08-27T14:39:00Z", + "iconUrl": "https://e7.pngegg.com/pngimages/775/350/png-clipart-action-plan-computer-icons-plan-miscellaneous-text-thumbnail.png", + "disabled": false, + "isPublic": true, + "profiles": [ "planning" ], + "mergeUtility": true, + "utilities": [], + "llmConfig": { + "provider": "openai", + "model": "gpt-4o-2024-11-20", + "max_recursion_depth": 10 + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/plan_primary_stage.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/plan_primary_stage.json new file mode 100644 index 00000000..5fefe691 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/plan_primary_stage.json @@ -0,0 +1,47 @@ +{ + "name": "plan_primary_stage", + "description": "Plan the high level steps to finish the task", + "parameters": { + "type": "object", + "properties": { + "requirement_detail": { + "type": "string", + "description": "User requirements related to data tasks in detail, don't miss any information especially for those line items, values and numbers." + }, + "questions": { + "type": "array", + "description": "Break down user data requirements in details and in multiple ways, don't miss any entity type/value. The output format must be string array.", + "items": { + "type": "string", + "description": "Question converted from requirement in different ways to search in the knowledge base, be short and you can refer to the global knowledge.One question should contain only one main topic that with one entity type." + } + }, + "norm_questions": { + "type": "array", + "description": "normalize the generated questions, remove specific entity value. The output format must be string array.", + "items": { + "type": "string", + "description": "Normalized question" + } + }, + "entities": { + "type": "array", + "description": "entities with type and value", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "entity type" + }, + "value": { + "type": "string", + "description": "entity value" + } + } + } + } + }, + "required": [ "requirement_detail", "questions" ] + } +} diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/plan_secondary_stage.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/plan_secondary_stage.json new file mode 100644 index 00000000..ca6b3d0b --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/plan_secondary_stage.json @@ -0,0 +1,18 @@ +{ + "name": "plan_secondary_stage", + "description": "Based on the primary stage planning, make more detail steps of the second stage if the primary stage needs more information.", + "parameters": { + "type": "object", + "properties": { + "task_description": { + "type": "string", + "description": "task description from primary steps" + }, + "solution_search_question": { + "type": "string", + "description": "Generate question to find the knowledge for text. Be short" + } + }, + "required": [ "task_description", "solution_search_question" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/sql_generation.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/sql_generation.json new file mode 100644 index 00000000..98e40785 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/sql_generation.json @@ -0,0 +1,26 @@ +{ + "name": "sql_generation", + "description": "Based on the planning steps, summarize the planning steps and output final steps.", + "parameters": { + "type": "object", + "properties": { + "is_sql_template": { + "type": "boolean", + "description": "If user request is to generate sql template instead of actual sql statement." + }, + "contains_sql_statements": { + "type": "boolean", + "description": "Set to true if the response contains sql statements." + }, + "related_tables": { + "type": "array", + "description": "table name in planning steps", + "items": { + "type": "string", + "description": "table name" + } + } + }, + "required": [ "related_tables", "is_sql_template", "contains_sql_statements" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/sql_review.json b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/sql_review.json new file mode 100644 index 00000000..ad360e3c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/functions/sql_review.json @@ -0,0 +1,22 @@ +{ + "name": "sql_review", + "description": "Verify and optimize sql statement", + "parameters": { + "type": "object", + "properties": { + "sql_statement": { + "type": "string", + "description": "sql statement, must including sql identifier that wrapped with ```sql \r\n```" + }, + "is_sql_template": { + "type": "boolean", + "description": "If user request is to generate sql template instead of actual sql statement." + }, + "contains_sql_statements": { + "type": "boolean", + "description": "Set to true if the response contains sql statements." + } + }, + "required": [ "sql_statement", "is_sql_template", "contains_sql_statements" ] + } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/instructions/instruction.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/instructions/instruction.liquid new file mode 100644 index 00000000..52e498e5 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/instructions/instruction.liquid @@ -0,0 +1,33 @@ +You're a SQL planner and reviewer, your goal is using function sql_generation and sql_review to response. +You are going convert the user requirement into sql statements. +The user is dealing with a complex problem, and you need to break this complex problem into several small tasks to more easily solve the user's needs. +Follow these steps strictly and in order. + +1. If user raised a new task, call plan_primary_stage to generate the primary plan. + If the sql response can be generate directly based on the context, directly go to step 6 to call function sql_review. +2. If need_lookup_dictionary is True, call verify_dictionary_term to verify or get the enum/term/dictionary value. Pull id and name. + * If you no items retured, you can pull 100 records from the table and look for the match. + * If need_lookup_dictionary is False, skip calling verify_dictionary_term. +3. If need_breakdown_task is true, call plan_secondary_stage for the specific primary stage. +4. Repeat step 3 until you processed all the primary steps. +5. Call sql_generation function to generate SQL statements. +6. Call sql_review function to review SQL statements. This is the step you must go through before reply to the user. + + +{% if global_knowledges != empty -%} +===== +Global Knowledge: +Current date time is: {{ "now" | date: "%Y-%m-%d %H:%M" }} +{% for k in global_knowledges %} +{{ k }} +{% endfor %} +===== +{%- endif %} + + +==== IMPORTANT SYSTEM INSTRUCTION ==== +* The verify_dictionary_term function CAN'T generate INSERT SQL Statement. +* The table name must come from the relevant knowledge. has_found_relevant_knowledge must be true. +* Do not introduce your actions or intentions in any way. +* You MUST explicitly call function sql_review even the sql query is provided in previous context. + diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.1st.plan.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.1st.plan.liquid new file mode 100644 index 00000000..306f4214 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.1st.plan.liquid @@ -0,0 +1,39 @@ +You are a Task Planner. you will breakdown user business requirements into excutable sub-tasks. + +Thinking process: +1. Reference to "Domain Knowledge" if there is relevant knowledge; +2. Breakdown task into subtasks. + - The subtask should contain all needed parameters for subsequent steps. + - If limited information provided and there are furture information needed, or miss relationship between steps, set the need_breakdown_task to true. + - If there is extra knowledge or relationship needed between steps, set the need_breakdown_task to true for both steps. + - If the solution mentioned "related solutions" is needed, set the need_breakdown_task to true. + - You should find the relationships between data structure based on the domain knowledge strictly. If lack of information, set the need_breakdown_task to true. + - If you need to lookup the dictionary to verify or get the enum/term/dictionary value(exclude example data from attachment), set the need_lookup_dictionary to true. + - Don't set need_lookup_dictionary to true for attachment data. + - Seperate the dictionary lookup and need additional information/knowledge into different subtask. +3. Input argument must reference to corresponding variable name that retrieved by previous steps, variable name must start with '@'; +4. Output all the subtasks as much detail as possible in JSON: [{{ response_format }}] +5. You can NOT generate the final query before calling function plan_summary. + +Note: +* If the task includes repeat steps,e.g.same steps for multiple elements, only generate a single detailed solution without repeating steps for each elements. + +{% if global_knowledges != empty -%} +===== +Global Knowledge: +{% for k in global_knowledges %} +{{ k }} +{% endfor %} +{%- endif %} + +{% if domain_knowledges != empty -%} +===== +Domain Knowledge: +{% for k in domain_knowledges %} +{{ k }} +{% endfor %} +{%- endif %} +===== + +Task description: +{{ task_description }} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.2nd.plan.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.2nd.plan.liquid new file mode 100644 index 00000000..81e22fe8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.2nd.plan.liquid @@ -0,0 +1,22 @@ +Reference to "Primary Planning" and the additional knowledge included. Breakdown task into multiple steps. +* The step should contains all needed parameters. +* The parameters can be extracted from the original task. +* You need to list all the steps in detail. Finding relationships should also be a step. +* When generate the steps, you should find the relationships between data structure based on the provided knowledge strictly. +* If need_lookup_dictionary is true, call verify_dictionary_term to verify or get the enum/term/dictionary value. Pull id and name/code. +* Output all the steps as much detail as possible in JSON: [{{ response_format }}] + +Additional Requirements: +* "output_results" is variable name that needed to be used in the next step. + +===== +Sub Task Description: +{{ task_description }} + +===== +Primary Planning: +{{ primary_plan }} + +===== +Additional Knowledge: +{{ additional_knowledge }} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.next.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.next.liquid new file mode 100644 index 00000000..f7388be6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.next.liquid @@ -0,0 +1,12 @@ +What is the next step based on the CONVERSATION? +Route to the last handling agent in priority. +{% if expected_next_action_agent != empty -%} +Expected next action agent is {{ expected_next_action_agent }}. +{%- else -%} +Next action agent is inferred based on user lastest response. +{%- endif %} +{% if expected_user_goal_agent != empty -%} +Expected user goal agent is {{ expected_user_goal_agent }}. +{%- else -%} +User goal agent is inferred based on user initial request. +{%- endif %} diff --git a/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.summarize.liquid b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.summarize.liquid new file mode 100644 index 00000000..ef0e3ae3 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Planner/data/agents/da7aad2c-8112-48a2-ab7b-1f87da524741/templates/two_stage.summarize.liquid @@ -0,0 +1,29 @@ +You are a planning summarizer. You will generate the final output in JSON format based on the task description, knowledge and related table structure and relationship. +Generate a simple business explaination of the quried data for the non tech audience. call sql_review as the final step after generating the sql statement. + +Requirements: +{{ summary_requirements }} + +===== +Task description: +{{ task_description }} + +===== +Global Knowledges: +{{ global_knowledges }} + +===== +Domain Knowledges: +{{ domain_knowledges }} + +===== +Dictionary Items: +{{ dictionary_items }} + +===== +Table Structure: +{{ table_structure }} + +===== +Attached Excel Information: +{{ excel_import_result }} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/BotSharp.Plugin.PythonInterpreter.csproj b/src/Plugins/BotSharp.Plugin.PythonInterpreter/BotSharp.Plugin.PythonInterpreter.csproj index b3293a0f..c5e3d4ff 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/BotSharp.Plugin.PythonInterpreter.csproj +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/BotSharp.Plugin.PythonInterpreter.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterAgentHook.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterAgentHook.cs deleted file mode 100644 index 64e0ec27..00000000 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterAgentHook.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace BotSharp.Plugin.PythonInterpreter.Hooks; - -public class InterpreterAgentHook : AgentHookBase -{ - private static string FUNCTION_NAME = "python_interpreter"; - - public override string SelfId => string.Empty; - - public InterpreterAgentHook(IServiceProvider services, AgentSettings settings) - : base(services, settings) - { - } - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.PythonInterpreter); - - if (isConvMode && isEnabled) - { - var (prompt, fn) = GetPromptAndFunction(); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, FunctionDef?) GetPromptAndFunction() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME)); - return (prompt, loadAttachmentFn); - } -} diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterUtilityHook.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterUtilityHook.cs index be37bfa1..540870be 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Hooks/InterpreterUtilityHook.cs @@ -2,8 +2,17 @@ namespace BotSharp.Plugin.PythonInterpreter.Hooks; public class InterpreterUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private static string FUNCTION_NAME = "python_interpreter"; + + public void AddUtilities(List utilities) { - utilities.Add(UtilityName.PythonInterpreter); + var utility = new AgentUtility() + { + Name = UtilityName.PythonInterpreter, + Functions = [new(FUNCTION_NAME)], + Templates = [new($"{FUNCTION_NAME}.fn")] + }; + + utilities.Add(utility); } } diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/InterpreterPlugin.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/InterpreterPlugin.cs index be2e7c06..7d195286 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/InterpreterPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/InterpreterPlugin.cs @@ -15,7 +15,6 @@ public class InterpreterPlugin : IBotSharpAppPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - services.AddScoped(); services.AddScoped(); } diff --git a/src/Plugins/BotSharp.Plugin.RoutingSpeeder/RoutingConversationHook.cs b/src/Plugins/BotSharp.Plugin.RoutingSpeeder/RoutingConversationHook.cs index b7fab29e..b65e45c5 100644 --- a/src/Plugins/BotSharp.Plugin.RoutingSpeeder/RoutingConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.RoutingSpeeder/RoutingConversationHook.cs @@ -42,7 +42,7 @@ public class RoutingConversationHook: ConversationHookBase // Render by template var templateService = _services.GetRequiredService(); - var response = await templateService.RenderIntentResponse(_agent.Id, message); + var response = await templateService.RenderIntentResponse(Agent.Id, message); if (!string.IsNullOrEmpty(response)) { @@ -54,7 +54,7 @@ public class RoutingConversationHook: ConversationHookBase public override async Task OnResponseGenerated(RoleDialogModel message) { var routerSettings = _services.GetRequiredService(); - bool saveFlag = _agent.Type != AgentType.Routing; + bool saveFlag = Agent.Type != AgentType.Routing; if (saveFlag) { @@ -63,7 +63,7 @@ public class RoutingConversationHook: ConversationHookBase var rootDataPath = agentService.GetDataDir(); string rawDataDir = Path.Combine(rootDataPath, "raw_data", $"agent.{message.CurrentAgentId}.txt"); - var lastThreeDialogs = _dialogs.Where(x => x.Role == AgentRole.User || x.Role == AgentRole.Assistant) + var lastThreeDialogs = Dialogs.Where(x => x.Role == AgentRole.User || x.Role == AgentRole.Assistant) .Select(x => x.Content.Replace('\r', ' ').Replace('\n', ' ')) .TakeLast(3) .ToArray(); diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj index 6433aae1..dbd3228d 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/BotSharp.Plugin.SqlDriver.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -17,12 +17,6 @@ - - - - - - @@ -36,22 +30,29 @@ + + + + + + + - + PreserveNewest - + PreserveNewest - + PreserveNewest PreserveNewest - + PreserveNewest @@ -69,10 +70,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest @@ -106,6 +107,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Enum/Utility.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Enum/Utility.cs deleted file mode 100644 index 4d9142ca..00000000 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Enum/Utility.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BotSharp.Plugin.SqlDriver.Enum; - -public class Utility -{ - public const string SqlExecutor = "sql-executor"; - public const string SqlDictionaryLookup = "sql-dictionary-lookup"; - public const string SqlTableDefinition = "sql-table-definition"; -} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Enum/UtilityName.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Enum/UtilityName.cs new file mode 100644 index 00000000..abb1faa2 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Enum/UtilityName.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Plugin.SqlDriver.Enum; + +public class UtilityName +{ + public const string SqlExecutor = "db.sql-executor"; + public const string SqlDictionaryLookup = "db.sql-dictionary-lookup"; + public const string SqlTableDefinition = "db.sql-table-definition"; +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs index 99060105..f571fa6c 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/ExecuteQueryFn.cs @@ -1,14 +1,8 @@ -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Routing; using BotSharp.Core.Infrastructures; -using BotSharp.Plugin.SqlDriver.Interfaces; -using BotSharp.Plugin.SqlDriver.Models; using Dapper; using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging; using MySqlConnector; using Npgsql; -using System.Data.Common; namespace BotSharp.Plugin.SqlDriver.Functions; @@ -113,6 +107,7 @@ public class ExecuteQueryFn : IFunctionCallback using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString); return connection.Query(string.Join("\r\n", sqlTexts)); } + private IEnumerable RunQueryInRedshift(string[] sqlTexts) { var settings = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs index cfc9765f..eb9253e4 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlSelect.cs @@ -1,6 +1,6 @@ -using BotSharp.Plugin.SqlDriver.Models; using Microsoft.Data.SqlClient; using MySqlConnector; +using Npgsql; using static Dapper.SqlMapper; namespace BotSharp.Plugin.SqlDriver.Functions; @@ -26,12 +26,15 @@ public class SqlSelect : IFunctionCallback } // check if need to instantely - var settings = _services.GetRequiredService(); - var result = settings.DatabaseType switch + var dbHook = _services.GetRequiredService(); + var dbType = dbHook.GetDatabaseType(message); + + var result = dbType switch { - "MySql" => RunQueryInMySql(args), - "SqlServer" => RunQueryInSqlServer(args), - _ => throw new NotImplementedException($"Database type {settings.DatabaseType} is not supported.") + "mysql" => RunQueryInMySql(args), + "sqlserver" => RunQueryInSqlServer(args), + "redshift" => RunQueryInRedshift(args), + _ => throw new NotImplementedException($"Database type {dbType} is not supported.") }; if (result == null) @@ -70,4 +73,16 @@ public class SqlSelect : IFunctionCallback } return connection.Query(args.Statement, dictionary); } + + private IEnumerable RunQueryInRedshift(SqlStatement args) + { + var settings = _services.GetRequiredService(); + using var connection = new NpgsqlConnection(settings.RedshiftConnectionString); + var dictionary = new Dictionary(); + foreach (var p in args.Parameters) + { + dictionary["@" + p.Name] = p.Value; + } + return connection.Query(args.Statement, dictionary); + } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs index f47e022d..b712d347 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Functions/SqlValidateFn.cs @@ -62,7 +62,7 @@ public class SqlValidateFn : IFunctionCallback { Provider = agent?.LlmConfig?.Provider ?? "openai", Model = agent?.LlmConfig?.Model ?? "gpt-4o", - Message = "Correct SQL Statement", + Message = "Correct SQL Statement and keep the comments/explanations", Data = new Dictionary { { "original_sql", message.Content }, diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Helpers/SqlDriverHelper.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Helpers/SqlDriverHelper.cs new file mode 100644 index 00000000..b8576c83 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Helpers/SqlDriverHelper.cs @@ -0,0 +1,20 @@ +namespace BotSharp.Plugin.SqlDriver.Helpers; + +internal static class SqlDriverHelper +{ + internal static string GetDatabaseType(IServiceProvider services) + { + var settings = services.GetRequiredService(); + var dbType = "mysql"; + + if (!string.IsNullOrWhiteSpace(settings?.SqlServerConnectionString)) + { + dbType = "sqlserver"; + } + else if (!string.IsNullOrWhiteSpace(settings?.SqlLiteConnectionString)) + { + dbType = "sqllite"; + } + return dbType; + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/GetTableDefinitionHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/GetTableDefinitionHook.cs deleted file mode 100644 index bcfd2093..00000000 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/GetTableDefinitionHook.cs +++ /dev/null @@ -1,84 +0,0 @@ -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; - -namespace BotSharp.Plugin.SqlDriver.Hooks; - -public class GetTableDefinitionHook : AgentHookBase, IAgentHook -{ - private const string SQL_EXECUTOR_TEMPLATE = "sql_table_definition.fn"; - private IEnumerable _targetSqlExecutorFunctions = new List - { - "sql_table_definition", - }; - - public override string SelfId => BuiltInAgentId.Planner; - - public GetTableDefinitionHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(Utility.SqlTableDefinition); - - if (isConvMode && isEnabled) - { - var (prompt, fns) = GetPromptAndFunctions(); - if (!fns.IsNullOrEmpty()) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = fns; - } - else - { - agent.Functions.AddRange(fns); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, List?) GetPromptAndFunctions() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var fns = agent?.Functions?.Where(x => _targetSqlExecutorFunctions.Contains(x.Name))?.ToList(); - - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(SQL_EXECUTOR_TEMPLATE))?.Content ?? string.Empty; - var dbType = GetDatabaseType(); - var render = _services.GetRequiredService(); - prompt = render.Render(prompt, new Dictionary - { - { "db_type", dbType } - }); - - return (prompt, fns); - } - - private string GetDatabaseType() - { - var settings = _services.GetRequiredService(); - var dbType = "MySQL"; - - if (!string.IsNullOrWhiteSpace(settings?.SqlServerConnectionString)) - { - dbType = "SQL Server"; - } - else if (!string.IsNullOrWhiteSpace(settings?.SqlLiteConnectionString)) - { - dbType = "SQL Lite"; - } - return dbType; - } -} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDictionaryLookupHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDictionaryLookupHook.cs deleted file mode 100644 index 534b0bf7..00000000 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDictionaryLookupHook.cs +++ /dev/null @@ -1,84 +0,0 @@ -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; - -namespace BotSharp.Plugin.SqlDriver.Hooks; - -public class SqlDictionaryLookupHook : AgentHookBase, IAgentHook -{ - private const string SQL_EXECUTOR_TEMPLATE = "verify_dictionary_term.fn"; - private IEnumerable _targetSqlExecutorFunctions = new List - { - "verify_dictionary_term", - }; - - public override string SelfId => BuiltInAgentId.Planner; - - public SqlDictionaryLookupHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(Utility.SqlDictionaryLookup); - - if (isConvMode && isEnabled) - { - var (prompt, fns) = GetPromptAndFunctions(); - if (!fns.IsNullOrEmpty()) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = fns; - } - else - { - agent.Functions.AddRange(fns); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, List?) GetPromptAndFunctions() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var fns = agent?.Functions?.Where(x => _targetSqlExecutorFunctions.Contains(x.Name))?.ToList(); - - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(SQL_EXECUTOR_TEMPLATE))?.Content ?? string.Empty; - var dbType = GetDatabaseType(); - var render = _services.GetRequiredService(); - prompt = render.Render(prompt, new Dictionary - { - { "db_type", dbType } - }); - - return (prompt, fns); - } - - private string GetDatabaseType() - { - var settings = _services.GetRequiredService(); - var dbType = "MySQL"; - - if (!string.IsNullOrWhiteSpace(settings?.SqlServerConnectionString)) - { - dbType = "SQL Server"; - } - else if (!string.IsNullOrWhiteSpace(settings?.SqlLiteConnectionString)) - { - dbType = "SQL Lite"; - } - return dbType; - } -} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverAgentHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverAgentHook.cs new file mode 100644 index 00000000..14205493 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverAgentHook.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.Agents.Settings; + +namespace BotSharp.Plugin.SqlDriver.Hooks; + +public class SqlDriverAgentHook : AgentHookBase, IAgentHook +{ + public override string SelfId => string.Empty; + + public SqlDriverAgentHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override void OnAgentLoaded(Agent agent) + { + var dbType = SqlDriverHelper.GetDatabaseType(_services); + agent.TemplateDict["db_type"] = dbType; + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverCrontabHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverCrontabHook.cs new file mode 100644 index 00000000..985da6b8 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverCrontabHook.cs @@ -0,0 +1,53 @@ +using BotSharp.Abstraction.Crontab.Models; +using BotSharp.Abstraction.Models; +using BotSharp.Abstraction.SideCar; +using BotSharp.Core.Crontab.Abstraction; + +namespace BotSharp.Plugin.SqlDriver.Hooks; + +public class SqlDriverCrontabHook : ICrontabHook +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + public SqlDriverCrontabHook(IServiceProvider services, ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task OnCronTriggered(CrontabItem item) + { + var conv = _services.GetRequiredService(); + conv.SetConversationId(item.ConversationId, []); + + _logger.LogWarning($"Crontab item triggered: {item.Title}. {item.Description}"); + + foreach (var task in item.Tasks) + { + if (task.Language == "text") + { + var sidecar = _services.GetService(); + var response = await sidecar.SendMessage(BuiltInAgentId.AIAssistant, $"{item.ExecutionResult}\r\n{task.Script}", states: new List()); + } + else if (task.Language == "sql") + { + var message = new RoleDialogModel(AgentRole.User, $"Run the query") + { + FunctionName = "sql_select", + FunctionArgs = JsonSerializer.Serialize(new SqlStatement + { + Statement = task.Script, + Reason = item.Description + }) + }; + var routing = _services.GetRequiredService(); + routing.Context.Push(BuiltInAgentId.SqlDriver); + await routing.InvokeFunction("sql_select", message); + + item.AgentId = BuiltInAgentId.SqlDriver; + item.UserId = "41021346"; + item.ExecutionResult += message.Content + "\r\n"; + } + } + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs index 94de1b04..224f8fd6 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlDriverPlanningHook.cs @@ -20,14 +20,23 @@ public class SqlDriverPlanningHook : IPlanningHook _services = services; } - public async Task OnPlanningCompleted(string planner, RoleDialogModel msg) + public async Task OnSourceCodeGenerated(string planner, RoleDialogModel msg, string language) { + // envoke validate + if (language != "sql") + { + return; + } + + var routing = _services.GetRequiredService(); + await routing.InvokeFunction("validate_sql", msg); + await HookEmitter.Emit(_services, async (hook) => { await hook.SqlGenerated(msg); }); - var settings = _services.GetRequiredService(); + var settings = _services.GetRequiredService(); if (!settings.ExecuteSqlSelectAutonomous) { var conversationStateService = _services.GetRequiredService(); @@ -51,7 +60,6 @@ public class SqlDriverPlanningHook : IPlanningHook var response = await completion.GetChatCompletions(agent, wholeDialogs); // Invoke "execute_sql" - var routing = _services.GetRequiredService(); await routing.InvokeFunction(response.FunctionName, response); msg.CurrentAgentId = agent.Id; @@ -61,6 +69,11 @@ public class SqlDriverPlanningHook : IPlanningHook msg.StopCompletion = response.StopCompletion; } + public async Task OnPlanningCompleted(string planner, RoleDialogModel msg) + { + + } + public async Task GetSummaryAdditionalRequirements(string planner, RoleDialogModel message) { var settings = _services.GetRequiredService(); diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlExecutorHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlExecutorHook.cs deleted file mode 100644 index 56e932c7..00000000 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlExecutorHook.cs +++ /dev/null @@ -1,85 +0,0 @@ -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; - -namespace BotSharp.Plugin.SqlDriver.Hooks; - -public class SqlExecutorHook : AgentHookBase, IAgentHook -{ - private const string SQL_EXECUTOR_TEMPLATE = "sql_executor.fn"; - private IEnumerable _targetSqlExecutorFunctions = new List - { - "sql_select", - "sql_table_definition", - }; - - public override string SelfId => string.Empty; - - public SqlExecutorHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(Utility.SqlExecutor); - - if (isConvMode && isEnabled) - { - var (prompt, fns) = GetPromptAndFunctions(); - if (!fns.IsNullOrEmpty()) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = fns; - } - else - { - agent.Functions.AddRange(fns); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, List?) GetPromptAndFunctions() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var fns = agent?.Functions?.Where(x => _targetSqlExecutorFunctions.Contains(x.Name))?.ToList(); - - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(SQL_EXECUTOR_TEMPLATE))?.Content ?? string.Empty; - var dbType = GetDatabaseType(); //need change-> using hook? - var render = _services.GetRequiredService(); - prompt = render.Render(prompt, new Dictionary - { - { "db_type", dbType } - }); - - return (prompt, fns); - } - - private string GetDatabaseType() - { - var settings = _services.GetRequiredService(); - var dbType = "MySQL"; - - if (!string.IsNullOrWhiteSpace(settings?.SqlServerConnectionString)) - { - dbType = "SQL Server"; - } - else if (!string.IsNullOrWhiteSpace(settings?.SqlLiteConnectionString)) - { - dbType = "SQL Lite"; - } - return dbType; - } -} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlUtilityHook.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlUtilityHook.cs index daac8cba..cdb65fc3 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Hooks/SqlUtilityHook.cs @@ -2,10 +2,36 @@ namespace BotSharp.Plugin.SqlDriver.Hooks; public class SqlUtilityHook : IAgentUtilityHook { - public void AddUtilities(List utilities) + private const string PREFIX = "util-db-"; + private const string SQL_TABLE_DEFINITION_FN = $"{PREFIX}sql_table_definition"; + private const string VERIFY_DICTIONARY_TERM_FN = $"{PREFIX}verify_dictionary_term"; + private const string SQL_SELECT_FN = $"{PREFIX}sql_select"; + private const string SQL_EXECUTOR_FN = $"{PREFIX}sql_executor"; + + public void AddUtilities(List utilities) { - utilities.Add(Utility.SqlExecutor); - utilities.Add(Utility.SqlDictionaryLookup); - utilities.Add(Utility.SqlTableDefinition); + var items = new List + { + new AgentUtility + { + Name = UtilityName.SqlTableDefinition, + Functions = [new(SQL_TABLE_DEFINITION_FN)], + Templates = [new($"{SQL_TABLE_DEFINITION_FN}.fn")] + }, + new AgentUtility + { + Name = UtilityName.SqlDictionaryLookup, + Functions = [new(VERIFY_DICTIONARY_TERM_FN)], + Templates = [new($"{VERIFY_DICTIONARY_TERM_FN}.fn")] + }, + new AgentUtility + { + Name = UtilityName.SqlExecutor, + Functions = [new(SQL_SELECT_FN), new(SQL_TABLE_DEFINITION_FN)], + Templates = [new($"{SQL_EXECUTOR_FN}.fn")] + } + }; + + utilities.AddRange(items); } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs index 24f12420..bff2c48c 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Settings/SqlDriverSetting.cs @@ -1,4 +1,4 @@ -namespace BotSharp.Plugin.SqlHero.Settings; +namespace BotSharp.Plugin.SqlDriver.Settings; public class SqlDriverSetting { diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs index 47a7883f..d6fe125b 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/SqlDriverPlugin.cs @@ -1,5 +1,5 @@ -using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Planning; +using BotSharp.Core.Crontab.Abstraction; namespace BotSharp.Plugin.SqlDriver; @@ -25,12 +25,11 @@ public class SqlDriverPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs index 9f63a841..b6b5ea06 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/Using.cs @@ -6,10 +6,8 @@ global using System.Text.RegularExpressions; global using System.Threading.Tasks; global using System.Linq; global using System.Text.Json; - global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.Logging; - global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Plugins; global using BotSharp.Abstraction.Conversations.Models; @@ -26,9 +24,10 @@ global using BotSharp.Abstraction.Settings; global using BotSharp.Plugin.SqlDriver.Hooks; global using BotSharp.Plugin.SqlDriver.Services; global using BotSharp.Plugin.SqlDriver.Enum; -global using BotSharp.Plugin.SqlHero.Settings; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Instructs; global using BotSharp.Abstraction.Instructs.Models; global using BotSharp.Abstraction.Routing; global using BotSharp.Plugin.SqlDriver.Interfaces; +global using BotSharp.Plugin.SqlDriver.Helpers; +global using BotSharp.Plugin.SqlDriver.Settings; diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs new file mode 100644 index 00000000..a4b493a3 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/GetTableDefinitionFn.cs @@ -0,0 +1,168 @@ +using Microsoft.Data.SqlClient; +using MySqlConnector; +using Npgsql; + +namespace BotSharp.Plugin.SqlDriver.UtilFunctions; + +public class GetTableDefinitionFn : IFunctionCallback +{ + public string Name => "util-db-sql_table_definition"; + public string Indication => "Obtain the relevant data structure definitions."; + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public GetTableDefinitionFn( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var tables = args.Tables; + var agentService = _services.GetRequiredService(); + var dbHook = _services.GetRequiredService(); + var dbType = dbHook.GetDatabaseType(message); + + // Get table DDL from database + var tableDdls = dbType switch + { + "mysql" => GetDdlFromMySql(tables), + "sqlserver" => GetDdlFromSqlServer(tables), + "redshift" => GetDdlFromRedshift(tables), + _ => throw new NotImplementedException($"Database type {dbType} is not supported.") + }; + + message.Content = string.Join("\r\n\r\n", tableDdls); + return true; + } + + private List GetDdlFromMySql(string[] tables) + { + var settings = _services.GetRequiredService(); + var tableDdls = new List(); + using var connection = new MySqlConnection(settings.MySqlMetaConnectionString ?? settings.MySqlConnectionString); + connection.Open(); + + foreach (var table in tables) + { + try + { + var escapedTableName = MySqlHelper.EscapeString(table); + var sql = $"SHOW CREATE TABLE `{escapedTableName}`"; + + using var command = new MySqlCommand(sql, connection); + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + var result = reader.GetString(1); + tableDdls.Add(result); + } + + reader.Close(); + command.Dispose(); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting ddl statement of table {table}. {ex.Message}\r\n{ex.InnerException}"); + } + } + + connection.Close(); + return tableDdls; + } + + private List GetDdlFromSqlServer(string[] tables) + { + var settings = _services.GetRequiredService(); + var tableDdls = new List(); + using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString); + connection.Open(); + + foreach (var table in tables) + { + try + { + var sql = @$"DECLARE @TableName NVARCHAR(128) = '{table}'; + DECLARE @SQL NVARCHAR(MAX) = 'CREATE TABLE ' + @TableName + ' ('; + + SELECT @SQL = @SQL + ' + ' + COLUMN_NAME + ' ' + + DATA_TYPE + + CASE + WHEN CHARACTER_MAXIMUM_LENGTH IS NOT NULL AND DATA_TYPE LIKE '%char%' + THEN '(' + CAST(CHARACTER_MAXIMUM_LENGTH AS VARCHAR(10)) + ')' + WHEN DATA_TYPE IN ('decimal', 'numeric') + THEN '(' + CAST(NUMERIC_PRECISION AS VARCHAR(10)) + ',' + CAST(NUMERIC_SCALE AS VARCHAR(10)) + ')' + ELSE '' + END + ' ' + + CASE WHEN IS_NULLABLE = 'NO' THEN 'NOT NULL' ELSE 'NULL' END + ',' + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = @TableName + ORDER BY ORDINAL_POSITION; + + -- Remove the last comma and add closing parenthesis + SET @SQL = LEFT(@SQL, LEN(@SQL) - 1) + ');'; + + SELECT @SQL;"; + + using var command = new SqlCommand(sql, connection); + using var reader = command.ExecuteReader(); + if (reader.Read()) + { + var result = reader.GetString(0); + tableDdls.Add(result); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting ddl statement of table {table}. {ex.Message}\r\n{ex.InnerException}"); + } + } + + connection.Close(); + return tableDdls; + } + + private List GetDdlFromRedshift(string[] tables) + { + var settings = _services.GetRequiredService(); + var tableDdls = new List(); + var schemas = "'onebi_hour','onebi_day'"; + using var connection = new NpgsqlConnection(settings.RedshiftConnectionString); + connection.Open(); + + foreach (var table in tables) + { + try + { + var sql = $"SELECT columnname, external_type, columnnum FROM svv_external_columns WHERE schemaname in ({schemas}) AND tablename = '{table}';"; + + using var command = new NpgsqlCommand(sql, connection); + using var reader = command.ExecuteReader(); + var ddl = $"Table {table} contains below fields: \r\n"; + while (reader.Read()) + { + /*var result = reader.GetString(0); + tableDdls.Add(result);*/ + var columnName = reader.GetString(0); + var dataType = reader.GetString(1); + ddl += $"{columnName} {dataType}\n"; + } + tableDdls.Add(ddl); + + reader.Close(); + command.Dispose(); + } + catch (Exception ex) + { + _logger.LogWarning($"Error when getting ddl statement of table {table}. {ex.Message}\r\n{ex.InnerException}"); + } + } + connection.Close(); + return tableDdls; + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs new file mode 100644 index 00000000..5ece6ecf --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/SqlSelect.cs @@ -0,0 +1,88 @@ +using Microsoft.Data.SqlClient; +using MySqlConnector; +using Npgsql; +using static Dapper.SqlMapper; + +namespace BotSharp.Plugin.SqlDriver.UtilFunctions; + +public class SqlSelect : IFunctionCallback +{ + public string Name => "util-db-sql_select"; + private readonly IServiceProvider _services; + + public SqlSelect(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + + if (args.GeneratedWithoutTableDefinition) + { + message.Content = $"Get the table definition first."; + return false; + } + + // check if need to instantely + var dbHook = _services.GetRequiredService(); + var dbType = dbHook.GetDatabaseType(message); + + var result = dbType switch + { + "mysql" => RunQueryInMySql(args), + "sqlserver" => RunQueryInSqlServer(args), + "redshift" => RunQueryInRedshift(args), + _ => throw new NotImplementedException($"Database type {dbType} is not supported.") + }; + + if (result == null) + { + message.Content = "Record not found"; + } + else + { + message.Content = JsonSerializer.Serialize(result); + args.Return.Value = message.Content; + } + + return true; + } + + private IEnumerable RunQueryInMySql(SqlStatement args) + { + var settings = _services.GetRequiredService(); + using var connection = new MySqlConnection(settings.MySqlExecutionConnectionString); + var dictionary = new Dictionary(); + foreach (var p in args.Parameters) + { + dictionary["@" + p.Name] = p.Value; + } + return connection.Query(args.Statement, dictionary); + } + + private IEnumerable RunQueryInSqlServer(SqlStatement args) + { + var settings = _services.GetRequiredService(); + using var connection = new SqlConnection(settings.SqlServerExecutionConnectionString ?? settings.SqlServerConnectionString); + var dictionary = new Dictionary(); + foreach (var p in args.Parameters) + { + dictionary["@" + p.Name] = p.Value; + } + return connection.Query(args.Statement, dictionary); + } + + private IEnumerable RunQueryInRedshift(SqlStatement args) + { + var settings = _services.GetRequiredService(); + using var connection = new NpgsqlConnection(settings.RedshiftConnectionString); + var dictionary = new Dictionary(); + foreach (var p in args.Parameters) + { + dictionary["@" + p.Name] = p.Value; + } + return connection.Query(args.Statement, dictionary); + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs new file mode 100644 index 00000000..2e6dbe72 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/UtilFunctions/VerifyDictionaryTerm.cs @@ -0,0 +1,130 @@ +using BotSharp.Core.Infrastructures; +using MySqlConnector; +using static Dapper.SqlMapper; + +namespace BotSharp.Plugin.SqlDriver.UtilFunctions; + +public class VerifyDictionaryTerm : IFunctionCallback +{ + public string Name => "util-db-verify_dictionary_term"; + public string Indication => "Verifying dictionary term"; + + + private readonly IServiceProvider _services; + + public VerifyDictionaryTerm(IServiceProvider services) + { + _services = services; + } + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + + // get table DDL + var fn = _services.GetRequiredService(); + var msgCopy = RoleDialogModel.From(message); + await fn.InvokeFunction("sql_table_definition", msgCopy); + + // refine SQL + var agentService = _services.GetRequiredService(); + var currentAgent = await agentService.LoadAgent(message.CurrentAgentId); + var dictionarySqlPrompt = await GetDictionarySQLPrompt(args.SqlStatement, msgCopy.Content); + var agent = new Agent + { + Id = message.CurrentAgentId ?? string.Empty, + Name = "sqlDriver_DictionarySearch", + Instruction = dictionarySqlPrompt, + TemplateDict = new Dictionary(), + LlmConfig = currentAgent.LlmConfig + }; + + var response = await GetAiResponse(agent); + args = response.Content.JsonContent(); + + // check if need to instantely + IEnumerable? result = null; + if (!string.IsNullOrWhiteSpace(args.SqlStatement)) + { + var settings = _services.GetRequiredService(); + using var connection = new MySqlConnection(settings.MySqlExecutionConnectionString); + result = connection.Query(args.SqlStatement); + } + + if (result.IsNullOrEmpty()) + { + message.Content = "Record not found"; + } + else + { + message.Content = JsonSerializer.Serialize(result); + } + + var states = _services.GetRequiredService(); + var dictionaryItems = states.GetState("dictionary_items", ""); + var newItem = BuildDictionaryItem(args.Table, args.Reason, message.Content); + dictionaryItems += !string.IsNullOrWhiteSpace(newItem) ? $"\r\n{newItem}\r\n" : string.Empty; + states.SetState("dictionary_items", dictionaryItems); + + return true; + } + + private async Task GetDictionarySQLPrompt(string originalSql, string tableStructure) + { + var agentService = _services.GetRequiredService(); + var render = _services.GetRequiredService(); + var knowledgeHooks = _services.GetServices(); + + var agent = await agentService.GetAgent(BuiltInAgentId.SqlDriver); + var template = agent.Templates.FirstOrDefault(x => x.Name == "database.dictionary.sql")?.Content ?? string.Empty; + var responseFormat = JsonSerializer.Serialize(new LookupDictionary{ }); + + return render.Render(template, new Dictionary + { + { "original_sql", originalSql }, + { "table_structure", tableStructure }, + { "response_format", responseFormat } + }); + } + + private async Task GetAiResponse(Agent agent) + { + var text = "Check and correct the SQL statement."; + var message = new RoleDialogModel(AgentRole.User, text); + + var completion = CompletionProvider.GetChatCompletion(_services, + provider: agent.LlmConfig.Provider, + model: agent.LlmConfig.Model); + + return await completion.GetChatCompletions(agent, new List { message }); + } + + private string BuildDictionaryItem(string? table, string? reason, string? result) + { + var res = string.Empty; + if (!string.IsNullOrWhiteSpace(table)) + { + res += $"Table: {table}"; + } + + if (!string.IsNullOrWhiteSpace(reason)) + { + if (!string.IsNullOrWhiteSpace(res)) + { + res += "\r\n"; + } + res += $"Reason: {reason}"; + } + + if (!string.IsNullOrWhiteSpace(result)) + { + if (!string.IsNullOrWhiteSpace(res)) + { + res += "\r\n"; + } + res += $"Result: {result}"; + } + + return res; + } +} diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/sql_select.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-sql_select.json similarity index 98% rename from src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/sql_select.json rename to src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-sql_select.json index 1bcf76a3..29f04789 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/sql_select.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-sql_select.json @@ -1,5 +1,5 @@ { - "name": "sql_select", + "name": "util-db-sql_select", "description": "Get the specific value from table", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/sql_table_definition.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-sql_table_definition.json similarity index 50% rename from src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/sql_table_definition.json rename to src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-sql_table_definition.json index e973feb8..0f808afe 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/sql_table_definition.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-sql_table_definition.json @@ -1,18 +1,22 @@ { - "name": "sql_table_definition", + "name": "util-db-sql_table_definition", "description": "Get table structure from database by table name", "parameters": { "type": "object", "properties": { - "table": { - "type": "string", - "description": "table name" + "tables": { + "type": "array", + "description": "table name in planning steps", + "items": { + "type": "string", + "description": "table name" + } }, "reason": { "type": "string", "description": "the reason why you need to call sql_table_definition" } }, - "required": [ "table", "reason" ] + "required": [ "tables", "reason" ] } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/verify_dictionary_term.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-verify_dictionary_term.json similarity index 96% rename from src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/verify_dictionary_term.json rename to src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-verify_dictionary_term.json index 64502c62..c354c105 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/verify_dictionary_term.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-db-verify_dictionary_term.json @@ -1,5 +1,5 @@ { - "name": "verify_dictionary_term", + "name": "util-db-verify_dictionary_term", "description": "Get id from dictionary table by keyword. Call this function only if need_lookup_dictionary is true, is_table_from_knowledge is true and is_insert is false. You can only query one table at a time. The table name must come from the global/domain knowledge.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/sql_table_definition.fn.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/sql_table_definition.fn.liquid deleted file mode 100644 index 9a8dee70..00000000 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/sql_table_definition.fn.liquid +++ /dev/null @@ -1 +0,0 @@ -Call sql_table_definition to get the table definition of the table. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/sql_executor.fn.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-sql_executor.fn.liquid similarity index 78% rename from src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/sql_executor.fn.liquid rename to src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-sql_executor.fn.liquid index edcb4bef..f3dd7faf 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/sql_executor.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-sql_executor.fn.liquid @@ -1,6 +1,6 @@ You are connecting to {{ db_type }} database. You can run provided SQL statements by following {{ db_type }} rules. -Please call function sql_select if user wants to get or retrieve data from data tables. +Please call function util-db-sql_select if user wants to get or retrieve data from data tables. If there are any parameters, please add them in the WHERE clause, each of which starts with "@". Avoid returning the entire record and only return the fields you need. For example, SELECT Id FROM table WHERE Id=@Id AND Name=@Name diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-sql_table_definition.fn.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-sql_table_definition.fn.liquid new file mode 100644 index 00000000..3d8ad563 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-sql_table_definition.fn.liquid @@ -0,0 +1 @@ +Call util-db-sql_table_definition to get the table definition of the table. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/verify_dictionary_term.fn.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-verify_dictionary_term.fn.liquid similarity index 92% rename from src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/verify_dictionary_term.fn.liquid rename to src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-verify_dictionary_term.fn.liquid index 6a9b06e8..12712215 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/verify_dictionary_term.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-db-verify_dictionary_term.fn.liquid @@ -1,4 +1,4 @@ -Rules to call verify_dictionary_term: +Rules to call util-db-verify_dictionary_term: ===== 1. The table name must come from the planning in conversation. 2. You must return the id and name/code for existing dictionary. diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json index 1d5aca1f..9f902a01 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/agent.json @@ -1,7 +1,7 @@ { "id": "beda4c12-e1ec-4b4b-b328-3df4a6687c4f", "name": "SQL Driver", - "description": "Execute the sql query in database from the latest dialog.", + "description": "Transfer to this Agent when user mentions to execute the sql statement. Only call when executable SQL statements are explicitly provided in the context.", "iconUrl": "https://cdn-icons-png.flaticon.com/512/3161/3161158.png", "type": "task", "createdDateTime": "2023-11-15T13:49:00Z", @@ -11,14 +11,14 @@ "profiles": [ "database" ], "llmConfig": { "provider": "openai", - "model": "gpt-4o" + "model": "gpt-4o-2024-11-20" }, "routingRules": [ { "field": "sql_statement", "required": true, "field_type": "string", - "description": "SQL statement" + "description": "SQL statement explicitly provided in the context." } ] } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_select.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_select.json index bd5b88ce..f2aa4c36 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_select.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_select.json @@ -1,12 +1,12 @@ { "name": "sql_select", - "description": "Execute the reporting related query in the database and get the result", + "description": "Execute the provided query in the database and get the result.", "parameters": { "type": "object", "properties": { "sql_statement": { "type": "string", - "description": "SQL statement with SELECT" + "description": "SQL statement with SELECT provided in context." }, "reason": { "type": "string", diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_table_definition.json b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_table_definition.json index 748202b4..580ee339 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_table_definition.json +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/functions/sql_table_definition.json @@ -4,11 +4,15 @@ "parameters": { "type": "object", "properties": { - "table": { - "type": "string", - "description": "table need to check" + "tables": { + "type": "array", + "description": "table name in planning steps", + "items": { + "type": "string", + "description": "table name" + } } }, - "required": [ "table" ] + "required": [ "tables" ] } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instructions/instruction.liquid b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instructions/instruction.liquid index 8daddf49..7cc97bb0 100644 --- a/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instructions/instruction.liquid +++ b/src/Plugins/BotSharp.Plugin.SqlDriver/data/agents/beda4c12-e1ec-4b4b-b328-3df4a6687c4f/instructions/instruction.liquid @@ -1,4 +1,4 @@ -You're a SQL driver who can find the database information or query the data. +You're a SQL driver who can execute the sql statement. Your response must meet below requirements: * You can only execute the SQL from the conversation. You can't generate one by yourself; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj b/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj index 662bf344..aff07e81 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj +++ b/src/Plugins/BotSharp.Plugin.Twilio/BotSharp.Plugin.Twilio.csproj @@ -7,17 +7,17 @@ $(GeneratePackageOnBuild) $(SolutionDir)packages - + - - + + - + - + PreserveNewest - + PreserveNewest diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs index 66abcd3a..63252a57 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Enums/UtilityName.cs @@ -2,6 +2,6 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums { public class UtilityName { - public const string OutboundPhoneCall = "twilio-outbound-phone-call"; + public const string OutboundPhoneCall = "twilio.twilio-outbound-phone-call"; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs index b9c77a71..a14564ba 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HandleOutboundPhoneCallFn.cs @@ -16,7 +16,7 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions private readonly BotSharpOptions _options; private readonly TwilioSetting _twilioSetting; - public string Name => "twilio_outbound_phone_call"; + public string Name => "util-twilio-twilio_outbound_phone_call"; public string Indication => "Dialing the number"; public HandleOutboundPhoneCallFn( diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerHook.cs deleted file mode 100644 index 7d8d33fb..00000000 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerHook.cs +++ /dev/null @@ -1,58 +0,0 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Agents.Settings; -using BotSharp.Abstraction.Repositories; -using BotSharp.Abstraction.Utilities; -using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums; - -namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks -{ - internal class OutboundPhoneCallHandlerHook : AgentHookBase - { - private static string FUNCTION_NAME = "twilio_outbound_phone_call"; - - public override string SelfId => string.Empty; - - public OutboundPhoneCallHandlerHook(IServiceProvider services, AgentSettings settings) : base(services, settings) - { - } - - public override void OnAgentLoaded(Agent agent) - { - var conv = _services.GetRequiredService(); - var isConvMode = conv.IsConversationMode(); - var isEnabled = !agent.Utilities.IsNullOrEmpty() && agent.Utilities.Contains(UtilityName.OutboundPhoneCall); - - if (isConvMode && isEnabled) - { - var (prompt, fn) = GetPromptAndFunction(); - if (fn != null) - { - if (!string.IsNullOrWhiteSpace(prompt)) - { - agent.Instruction += $"\r\n\r\n{prompt}\r\n\r\n"; - } - - if (agent.Functions == null) - { - agent.Functions = new List { fn }; - } - else - { - agent.Functions.Add(fn); - } - } - } - - base.OnAgentLoaded(agent); - } - - private (string, FunctionDef) GetPromptAndFunction() - { - var db = _services.GetRequiredService(); - var agent = db.GetAgent(BuiltInAgentId.UtilityAssistant); - var prompt = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo($"{FUNCTION_NAME}.fn"))?.Content ?? string.Empty; - var loadAttachmentFn = agent?.Functions?.FirstOrDefault(x => x.Name.IsEqualTo(FUNCTION_NAME)); - return (prompt, loadAttachmentFn); - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs index 982fe9b1..32639c9a 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Hooks/OutboundPhoneCallHandlerUtilityHook.cs @@ -1,12 +1,22 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Enums; -namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks +namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Hooks; + +public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook { - public class OutboundPhoneCallHandlerUtilityHook : IAgentUtilityHook + private static string PREFIX = "util-twilio-"; + private static string OUTBOUND_PHONE_CALL_FN = $"{PREFIX}twilio_outbound_phone_call"; + + public void AddUtilities(List utilities) { - public void AddUtilities(List utilities) + var utility = new AgentUtility { - utilities.Add(UtilityName.OutboundPhoneCall); - } + Name = UtilityName.OutboundPhoneCall, + Functions = [new($"{OUTBOUND_PHONE_CALL_FN}")], + Templates = [new($"{OUTBOUND_PHONE_CALL_FN}.fn")] + }; + + utilities.Add(utility); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs b/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs index 484a5eaf..d78489ad 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs @@ -31,7 +31,6 @@ public class TwilioPlugin : IBotSharpPlugin services.AddSingleton(); services.AddHostedService(); services.AddTwilioRequestValidation(); - services.AddScoped(); services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/twilio_outbound_phone_call.json b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-twilio_outbound_phone_call.json similarity index 92% rename from src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/twilio_outbound_phone_call.json rename to src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-twilio_outbound_phone_call.json index 8a49f441..768c6fde 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/twilio_outbound_phone_call.json +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-twilio_outbound_phone_call.json @@ -1,5 +1,5 @@ { - "name": "twilio_outbound_phone_call", + "name": "util-twilio-twilio_outbound_phone_call", "description": "If the user wants to initiate a phone call, you need to capture the phone number and compose the message the users wants to send. Then call this function to make an outbound call via Twilio.", "parameters": { "type": "object", diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/twilio_outbound_phone_call.fn.liquid b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-twilio_outbound_phone_call.fn.liquid similarity index 51% rename from src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/twilio_outbound_phone_call.fn.liquid rename to src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-twilio_outbound_phone_call.fn.liquid index 55210a34..3bfb7dc2 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/twilio_outbound_phone_call.fn.liquid +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/templates/util-twilio-twilio_outbound_phone_call.fn.liquid @@ -1,2 +1,2 @@ ** Please take a look at the conversation and decide whether user wants to make an outbound call. -** Please call handle_outbound_call if user wants to make an outbound call. \ No newline at end of file +** Please call util-twilio-twilio_outbound_phone_call if user wants to make an outbound call. \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs index f7884beb..40fec84e 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.DoAction.cs @@ -79,10 +79,102 @@ public partial class PlaywrightWebDriver { await locator.HoverAsync(); } + else if (action.Action == BroswerActionEnum.DragAndDrop) + { + // Locate the element to drag + var box = await locator.BoundingBoxAsync(); + + if (box != null) + { + // Calculate start position + float startX = box.X + box.Width / 2; // Start at the center of the element + float startY = box.Y + box.Height / 2; + + // Drag offsets + float offsetX = action.Position.X; + // Move horizontally + if (action.Position.Y == 0) + { + // Perform drag-and-move + // Move mouse to the start position + var mouse = page.Mouse; + await mouse.MoveAsync(startX, startY); + await mouse.DownAsync(); + + // Move mouse smoothly in increments + var tracks = GetVelocityTrack(offsetX); + foreach (var track in tracks) + { + startX += track; + await page.Mouse.MoveAsync(startX, 0, new MouseMoveOptions + { + Steps = 3 + }); + } + + // Release mouse button + await mouse.UpAsync(); + } + else + { + throw new NotImplementedException(); + } + } + } if (action.WaitTime > 0) { await Task.Delay(1000 * action.WaitTime); } } + + public static List GetVelocityTrack(float distance) + { + // Initialize the track list to store the movement distances + List track = new List(); + + // Initialize variables + float current = 0; // Current position + float mid = distance * 4 / 5; // Deceleration threshold + float t = 0.2f; // Time interval + float v = 1; // Initial velocity + + // Generate the track + while (current < distance) + { + float a; // Acceleration + + // Determine acceleration based on position + if (current < mid) + { + a = 4; // Accelerate + } + else + { + a = -3; // Decelerate + } + + // Calculate new velocity + float v0 = v; + v = v0 + a * t; + + // Calculate the movement during this interval + float move = v0 * t + 0.5f * a * t * t; + + // Update current position + if (current + move > distance) + { + move = distance - current; + track.Add((int)Math.Round(move)); + break; + } + + current += move; + + // Add rounded movement to the track + track.Add((int)Math.Round(move)); + } + + return track; + } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs index 44916a89..2f339ca6 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs @@ -8,7 +8,7 @@ public partial class PlaywrightWebDriver var context = await _instance.GetContext(message.ContextId); try { - var page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback, + var page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback, responseInMemory: args.ResponseInMemory, responseContainer: args.ResponseContainer, excludeResponseUrls: args.ExcludeResponseUrls, @@ -18,7 +18,7 @@ public partial class PlaywrightWebDriver if (args.OpenNewTab && page != null && page.Url == "about:blank") { - page = await _instance.NewPage(message, + page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback, responseInMemory: args.ResponseInMemory, responseContainer: args.ResponseContainer, @@ -28,7 +28,7 @@ public partial class PlaywrightWebDriver if (page == null) { - page = await _instance.NewPage(message, + page = await _instance.NewPage(message, enableResponseCallback: args.EnableResponseCallback, responseInMemory: args.ResponseInMemory, responseContainer: args.ResponseContainer, @@ -47,7 +47,7 @@ public partial class PlaywrightWebDriver if (args.Selectors != null) { // 使用传入的选择器列表进行并行等待 - var tasks =args.Selectors.Select(selector => + var tasks = args.Selectors.Select(selector => page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions { Timeout = args.Timeout > 0 ? args.Timeout : 30000 @@ -92,7 +92,7 @@ public partial class PlaywrightWebDriver } } else - { + { result.Message = response.StatusText; } } diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs index 48538ce8..3919629e 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.ScrollPage.cs @@ -5,7 +5,8 @@ public partial class PlaywrightWebDriver public async Task ScrollPage(MessageInfo message, PageActionArgs args) { var result = new BrowserActionResult(); - await _instance.Wait(message.ContextId); + var waitTime = args.WaitTime > 0 ? args.WaitTime : 10; + await _instance.Wait(message.ContextId, waitTime, args.WaitForNetworkIdle); var page = _instance.GetPage(message.ContextId); @@ -31,7 +32,7 @@ public partial class PlaywrightWebDriver int scrollY = await page.EvaluateAsync("document.body.scrollHeight"); // Scroll to the bottom - await page.Mouse.WheelAsync(0, -scrollY); + await page.Mouse.WheelAsync(0, scrollY); } else if (args.Direction == "top") { diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index a8f819d6..cb107490 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -29,6 +29,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 8c4cd9aa..c3d4bdd5 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -328,6 +328,7 @@ "Assemblies": [ "BotSharp.Core", "BotSharp.Core.SideCar", + "BotSharp.Core.Crontab", "BotSharp.Logger", "BotSharp.Plugin.MongoStorage", "BotSharp.Plugin.Dashboard", 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 4da6bdd3..94746011 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 @@ -12,8 +12,8 @@ "profiles": [ "pizza" ], "routingRules": [ { - "type": "planner", - "field": "NaivePlanner" + "type": "reasoner", + "field": "NaiveReasoner" } ] } \ No newline at end of file diff --git a/tests/UnitTest/MainTest.cs b/tests/UnitTest/MainTest.cs new file mode 100644 index 00000000..307855e4 --- /dev/null +++ b/tests/UnitTest/MainTest.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.DependencyInjection; +using BotSharp.Abstraction.Conversations; + +namespace UnitTest +{ + [TestClass] + public class MainTest + { + [TestMethod] + public void TestConversationHookProvider() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + var serviceProvider = services.BuildServiceProvider(); + var conversationHookProvider = serviceProvider.GetService(); + + Assert.AreEqual(3, conversationHookProvider.Hooks.Count()); + + var prevHook = default(IConversationHook); + + // Assert priority + foreach (var hook in conversationHookProvider.HooksOrderByPriority) + { + if (prevHook != null) + { + Assert.IsTrue(prevHook.Priority < hook.Priority); + } + + prevHook = hook; + } + } + + class TestHookA : ConversationHookBase + { + public TestHookA() + { + Priority = 1; + } + } + + class TestHookB : ConversationHookBase + { + public TestHookB() + { + Priority = 2; + } + } + + class TestHookC : ConversationHookBase + { + public TestHookC() + { + Priority = 3; + } + } + } +} \ No newline at end of file diff --git a/tests/UnitTest/UnitTest.csproj b/tests/UnitTest/UnitTest.csproj index be924573..67e7d504 100644 --- a/tests/UnitTest/UnitTest.csproj +++ b/tests/UnitTest/UnitTest.csproj @@ -13,10 +13,14 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/tests/UnitTest/UnitTest1.cs b/tests/UnitTest/UnitTest1.cs deleted file mode 100644 index ab3cd866..00000000 --- a/tests/UnitTest/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace UnitTest -{ - [TestClass] - public class UnitTest1 - { - [TestMethod] - public void TestMethod1() - { - } - } -} \ No newline at end of file