add http handler

This commit is contained in:
Jicheng Lu 2024-05-20 16:50:45 -05:00
parent 3b89064f32
commit d901fff30f
10 changed files with 302 additions and 5 deletions

View file

@ -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;
/// <summary>
/// This is the default LLM config for agent

View file

@ -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;
}

View file

@ -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<AgentSettings>();
if (settings.EnableHttpHandler)
{
var httpHandlerName = "handle_http_request";
var existHttpHandler = functions.Any(x => x.Name == httpHandlerName);
var funcs = _services.GetServices<IFunctionCallback>();
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<JsonDocument>(json),
Required = new List<string>
{
"request_url",
"http_method"
}
}
});
}
}
}
return base.OnFunctionsLoaded(functions);

View file

@ -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;

View file

@ -28,6 +28,10 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>

View file

@ -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<HandleHttpRequest> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _context;
private readonly BotSharpOptions _options;
public HandleHttpRequest(IServiceProvider services,
ILogger<HandleHttpRequest> logger,
IHttpClientFactory httpClientFactory,
IHttpContextAccessor context,
BotSharpOptions options)
{
_services = services;
_logger = logger;
_httpClientFactory = httpClientFactory;
_context = context;
_options = options;
}
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<LlmContextIn>(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<HttpResponseMessage?> SendHttpRequest(string? url, string? method, string? content)
{
if (string.IsNullOrEmpty(url)) return null;
var settings = _services.GetRequiredService<HttpSettings>();
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<HttpSettings>();
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<JsonDocument>(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<string>();
var json = JsonSerializer.Deserialize<JsonDocument>(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<string> HandleHttpResponse(HttpResponseMessage? response)
{
if (response == null) return string.Empty;
return await response.Content.ReadAsStringAsync();
}
private RichContent<IRichMessage> BuildRichContent(string? content)
{
var state = _services.GetRequiredService<IConversationStateService>();
var text = !string.IsNullOrEmpty(content) ? content : "Cannot get any response from the http request.";
return new RichContent<IRichMessage>
{
Recipient = new Recipient { Id = state.GetConversationId() },
Editor = EditorTypeEnum.Text,
Message = new TextMessage(text)
};
}
}

View file

@ -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<ISettingService>();
return settingService.Bind<HttpSettings>("Http");
});
}
}

View file

@ -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; }
}

View file

@ -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;
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;

View file

@ -141,6 +141,11 @@
"Driver": "Playwright"
},
"Http": {
"BaseAddress": "",
"Origin": ""
},
"Statistics": {
"DataDir": "stats"
},