From d901fff30fb3fcd8659ff0210cfd4ee33cda94fe Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Mon, 20 May 2024 16:50:45 -0500 Subject: [PATCH] add http handler --- .../Agents/Settings/AgentSettings.cs | 1 + .../Http/Settings/HttpSettings.cs | 7 + .../Routing/Hooks/RoutingAgentHook.cs | 49 +++- src/Infrastructure/BotSharp.Core/Using.cs | 1 + .../BotSharp.Plugin.HttpHandler.csproj | 4 + .../Functions/HandleHttpRequest.cs | 210 ++++++++++++++++++ .../HttpHandlerPlugin.cs | 8 +- .../LlmContexts/LlmContextIn.cs | 15 ++ .../BotSharp.Plugin.HttpHandler/Using.cs | 7 +- src/WebStarter/appsettings.json | 5 + 10 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs create mode 100644 src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs create mode 100644 src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs index 232e7b60..fefda240 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs @@ -6,6 +6,7 @@ public class AgentSettings public string TemplateFormat { get; set; } = "liquid"; public string HostAgentId { get; set; } = string.Empty; public bool EnableTranslator { get; set; } = false; + public bool EnableHttpHandler { get; set; } = false; /// /// This is the default LLM config for agent diff --git a/src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs new file mode 100644 index 00000000..8fc14988 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Http/Settings/HttpSettings.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Http.Settings; + +public class HttpSettings +{ + public string BaseAddress { get; set; } = string.Empty; + public string Origin { get; set; } = string.Empty; +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs index fa8c5db3..4aec88c8 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Hooks/RoutingAgentHook.cs @@ -1,8 +1,6 @@ -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Enums; using BotSharp.Abstraction.Routing.Settings; -using System.Diagnostics.Metrics; namespace BotSharp.Core.Routing.Hooks; @@ -106,6 +104,51 @@ public class RoutingAgentHook : AgentHookBase } }); } + + var settings = _services.GetRequiredService(); + if (settings.EnableHttpHandler) + { + var httpHandlerName = "handle_http_request"; + var existHttpHandler = functions.Any(x => x.Name == httpHandlerName); + var funcs = _services.GetServices(); + var httpRequestFunc = funcs.FirstOrDefault(x => x.Name == httpHandlerName); + if (!existHttpHandler && httpRequestFunc != null) + { + var json = JsonSerializer.Serialize(new + { + request_url = new + { + type = "string", + description = $"The http url that is requested. It can be an absolute url that starts with \"http\" or \"https\", or a relative url that starts with \"/\"" + }, + http_method = new + { + type = "string", + description = $"The http method that is requested, e.g., GET, POST, PUT, and DELETE." + }, + request_content = new + { + type = "string", + description = $"The http request content. It must be in json format." + } + }); + functions.Add(new FunctionDef + { + Name = httpRequestFunc.Name, + 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 = + { + Properties = JsonSerializer.Deserialize(json), + Required = new List + { + "request_url", + "http_method" + } + } + }); + } + + } } return base.OnFunctionsLoaded(functions); diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index 0c6fe5e7..a54df964 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -27,6 +27,7 @@ global using BotSharp.Abstraction.Files; global using BotSharp.Abstraction.Files.Models; global using BotSharp.Abstraction.Translation.Attributes; global using BotSharp.Abstraction.Messaging.Enums; +global using BotSharp.Abstraction.Http.Settings; global using BotSharp.Core.Repository; global using BotSharp.Core.Routing; global using BotSharp.Core.Agents.Services; diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj index 15ceb9d3..2607cb15 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/BotSharp.Plugin.HttpHandler.csproj @@ -28,6 +28,10 @@ + + + + diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs new file mode 100644 index 00000000..fae43abf --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Functions/HandleHttpRequest.cs @@ -0,0 +1,210 @@ +using System.Net.Http; +using BotSharp.Plugin.HttpHandler.LlmContexts; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace BotSharp.Plugin.HttpHandler.Functions; + +public class HandleHttpRequest : IFunctionCallback +{ + public string Name => "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 HandleHttpRequest(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.RichContent = BuildRichContent(responseContent); + return await Task.FromResult(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.RichContent = BuildRichContent($"{msg}"); + return await Task.FromResult(false); + } + } + + private async Task SendHttpRequest(string? url, string? method, string? content) + { + if (string.IsNullOrEmpty(url)) return null; + + var settings = _services.GetRequiredService(); + using var client = _httpClientFactory.CreateClient(); + AddRequestHeaders(client); + + var (uri, request) = BuildHttpRequest(url, method, content); + if (string.IsNullOrEmpty(uri.Host)) + { + client.BaseAddress = new Uri(settings.BaseAddress); + } + + var response = await client.SendAsync(request); + + if (response == null || !response.IsSuccessStatusCode) + { + throw new Exception($"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; + + if (httpMethod == HttpMethod.Get) + { + httpContent = BuildHttpContent(string.Empty); + } + else + { + httpContent = BuildHttpContent(content); + } + + var requestUrl = BuildQuery(url, content); + var uri = new Uri(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 ?? string.Empty, _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, "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; + var value = prop.Value.ToString(); + 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(); + } + + private RichContent BuildRichContent(string? content) + { + var state = _services.GetRequiredService(); + + var text = !string.IsNullOrEmpty(content) ? content : "Cannot get any response from the http request."; + return new RichContent + { + Recipient = new Recipient { Id = state.GetConversationId() }, + Editor = EditorTypeEnum.Text, + Message = new TextMessage(text) + }; + } +} diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs index b64a3ad8..3e31c096 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/HttpHandlerPlugin.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Http.Settings; +using BotSharp.Abstraction.Settings; using Microsoft.Extensions.Configuration; namespace BotSharp.Plugin.HttpHandler; @@ -12,6 +14,10 @@ public class HttpHandlerPlugin : IBotSharpPlugin public void RegisterDI(IServiceCollection services, IConfiguration config) { - + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("Http"); + }); } } diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs new file mode 100644 index 00000000..8d7f506e --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/LlmContexts/LlmContextIn.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.HttpHandler.LlmContexts; + +public class LlmContextIn +{ + [JsonPropertyName("request_url")] + public string? RequestUrl { get; set; } + + [JsonPropertyName("http_method")] + public string? HttpMethod { get; set; } + + [JsonPropertyName("request_content")] + public string? RequestContent { get; set; } +} diff --git a/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs b/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs index f813420b..4344b430 100644 --- a/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs +++ b/src/Plugins/BotSharp.Plugin.HttpHandler/Using.cs @@ -11,4 +11,9 @@ global using BotSharp.Abstraction.Agents.Models; global using BotSharp.Abstraction.Templating; global using Microsoft.Extensions.DependencyInjection; global using System.Linq; -global using BotSharp.Abstraction.Utilities; \ No newline at end of file +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Abstraction.Messaging; +global using BotSharp.Abstraction.Messaging.Models.RichContent; +global using BotSharp.Abstraction.Options; +global using BotSharp.Abstraction.Http.Settings; +global using BotSharp.Abstraction.Messaging.Enums; \ No newline at end of file diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 5482e6b1..e141d502 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -141,6 +141,11 @@ "Driver": "Playwright" }, + "Http": { + "BaseAddress": "", + "Origin": "" + }, + "Statistics": { "DataDir": "stats" },