This commit is contained in:
Haiping Chen 2024-03-22 07:53:54 -05:00
commit 80995f54df
17 changed files with 247 additions and 103 deletions

View file

@ -0,0 +1,14 @@
namespace BotSharp.Abstraction.Google.Models;
public class GoogleAddressResult
{
public IList<GoogleAddress> Results { get; set; } = new List<GoogleAddress>();
public string Status { get; set; }
}
public class GoogleAddress
{
[JsonPropertyName("formatted_address")]
public string FormatedAddress { get; set; }
}

View file

@ -0,0 +1,9 @@
namespace BotSharp.Abstraction.Google.Settings;
public class GoogleApiSettings
{
public string ApiKey { get; set; }
public string Endpoint { get; set; }
public string Language { get; set; }
public string Components { get; set; }
}

View file

@ -18,6 +18,9 @@ public class ButtonTemplateMessage : IRichMessage, ITemplateMessage
[JsonPropertyName("buttons")]
public ButtonElement[] Buttons { get; set; } = new ButtonElement[0];
[JsonPropertyName("is_horizontal")]
public bool IsHorizontal { get; set; }
}
public class ButtonElement
@ -34,4 +37,7 @@ public class ButtonElement
public string? Payload { get; set; }
public string Title { get; set; } = string.Empty;
[JsonPropertyName("is_primary")]
public bool IsPrimary { get; set; }
}

View file

@ -16,6 +16,9 @@ public class GenericTemplateMessage<T> : IRichMessage, ITemplateMessage
[JsonPropertyName("elements")]
public List<T> Elements { get; set; } = new List<T>();
[JsonPropertyName("is_horizontal")]
public bool IsHorizontal { get; set; }
[JsonPropertyName("element_type")]
public string ElementType => typeof(T).Name;
}

View file

@ -11,7 +11,12 @@ public class MultiSelectTemplateMessage : IRichMessage, ITemplateMessage
[JsonPropertyName("template_type")]
public string TemplateType => TemplateTypeEnum.MultiSelect;
[JsonPropertyName("options")]
public List<OptionElement> Options { get; set; } = new List<OptionElement>();
[JsonPropertyName("is_horizontal")]
public bool IsHorizontal { get; set; }
}
public class OptionElement

View file

@ -43,4 +43,6 @@ public interface IRoutingService
Task<RoleDialogModel> InstructDirect(Agent agent, RoleDialogModel message);
Task<string> GetConversationContent(List<RoleDialogModel> dialogs, int maxDialogCount = 50);
bool HasMissingRequiredField(RoleDialogModel message, out string agentId);
}

View file

@ -4,9 +4,6 @@ using Microsoft.Extensions.Configuration;
using BotSharp.Core.Plugins;
using BotSharp.Abstraction.Settings;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Messaging;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using BotSharp.Abstraction.Messaging.JsonConverters;
namespace BotSharp.Core;

View file

@ -1,13 +1,12 @@
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Routing.Models;
using System.Drawing;
namespace BotSharp.Core.Routing;
/// <summary>
/// Router calls this function to set the Active Agent according to the context
/// </summary>
public class RouteToAgentFn : IFunctionCallback
public partial class RouteToAgentFn : IFunctionCallback
{
public string Name => "route_to_agent";
private readonly IServiceProvider _services;
@ -82,7 +81,8 @@ public class RouteToAgentFn : IFunctionCallback
return false;
}
var missingfield = HasMissingRequiredField(message, out var agentId);
var routing = _services.GetRequiredService<IRoutingService>();
var missingfield = routing.HasMissingRequiredField(message, out var agentId);
if (missingfield && message.CurrentAgentId != agentId)
{
// Stack redirection agent
@ -94,97 +94,4 @@ public class RouteToAgentFn : IFunctionCallback
return true;
}
/// <summary>
/// If the target agent needs some required fields but the
/// </summary>
/// <returns></returns>
private bool HasMissingRequiredField(RoleDialogModel message, out string agentId)
{
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
var routing = _services.GetRequiredService<IRoutingService>();
var routingRules = routing.GetRulesByAgentName(args.AgentName);
if (routingRules == null || !routingRules.Any())
{
agentId = message.CurrentAgentId;
return false;
}
agentId = routingRules.First().AgentId;
// Add routed agent
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "route_to", agentId);
// Check required fields
var root = JsonSerializer.Deserialize<JsonElement>(message.FunctionArgs);
var missingFields = new List<string>();
foreach (var field in routingRules.Where(x => x.Required).Select(x => x.Field))
{
if (!root.EnumerateObject().Any(x => x.Name == field))
{
missingFields.Add(field);
}
else if (root.EnumerateObject().Any(x => x.Name == field) &&
string.IsNullOrEmpty(root.EnumerateObject().FirstOrDefault(x => x.Name == field).Value.ToString()))
{
missingFields.Add(field);
}
}
// Check if states contains the field according conversation context.
var states = _services.GetRequiredService<IConversationStateService>();
foreach (var field in missingFields.ToList())
{
if (!string.IsNullOrEmpty(states.GetState(field)))
{
var value = states.GetState(field);
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, field, value);
missingFields.Remove(field);
}
}
if (missingFields.Any())
{
// Add field to args
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "missing_fields", missingFields);
message.Content = $"missing some information: {string.Join(", ", missingFields)}";
// Handle redirect
var routingRule = routingRules.FirstOrDefault(x => missingFields.Contains(x.Field));
if (!string.IsNullOrEmpty(routingRule.RedirectTo))
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgent(routingRule.RedirectTo);
// Add redirected agent
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name);
agentId = routingRule.RedirectTo;
var logger = _services.GetRequiredService<ILogger<RouteToAgentFn>>();
#if DEBUG
Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow);
#else
logger.LogInformation($"*** Routing redirect to {record.Name.ToUpper()} ***");
#endif
}
else
{
// back to router
agentId = message.CurrentAgentId;
}
}
return missingFields.Any();
}
private string AppendPropertyToArgs(string args, string key, string value)
{
return args.Substring(0, args.Length - 1) + $", \"{key}\": \"{value}\"" + "}";
}
private string AppendPropertyToArgs(string args, string key, IEnumerable<string> values)
{
string fields = string.Join(",", values.Select(x => $"\"{x}\""));
return args.Substring(0, args.Length - 1) + $", \"{key}\": [{fields}]" + "}";
}
}

View file

@ -10,7 +10,7 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
public List<ParameterPropertyDef> Parameters => new List<ParameterPropertyDef>
{
new ParameterPropertyDef("next_action_reason", "the reason why route to this agent, if user is replying last agent's question, you must route to this agent")
new ParameterPropertyDef("next_action_reason", "the reason why route to this agent")
{
Required = true
},
@ -22,7 +22,7 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler
{
Required = true
},
new ParameterPropertyDef("user_goal_agent", "agent who can achieve user original goal")
new ParameterPropertyDef("user_goal_agent", "user original goal")
{
Required = true
},

View file

@ -105,6 +105,31 @@ public class RoutingContext : IRoutingContext
HookEmitter.Emit<IRoutingHook>(_services, async hook =>
await hook.OnAgentDequeued(agentId, currentAgentId, reason: reason)
).Wait();
// Run the routing rule
var agency = _services.GetRequiredService<IAgentService>();
var agent = agency.LoadAgent(currentAgentId).Result;
var message = new RoleDialogModel(AgentRole.User, $"Try to route to agent {agent.Name}")
{
FunctionName = "route_to_agent",
FunctionArgs = JsonSerializer.Serialize(new FunctionCallFromLlm
{
Function = "route_to_agent",
AgentName = agent.Name,
Reason = $"User manually route to agent {agent.Name}"
})
};
var routing = _services.GetRequiredService<IRoutingService>();
var missingfield = routing.HasMissingRequiredField(message, out agentId);
if (missingfield)
{
if (currentAgentId != agentId)
{
_stack.Push(agentId);
}
}
}
public string PreviousAgentId()
@ -115,7 +140,7 @@ public class RoutingContext : IRoutingContext
}
else if (_stack.Count > 1)
{
return _stack.ToArray()[1];
return _stack.ToArray()[_stack.Count - 2];
}
return string.Empty;

View file

@ -0,0 +1,100 @@
using BotSharp.Abstraction.Routing.Models;
using System.Drawing;
namespace BotSharp.Core.Routing;
public partial class RoutingService
{
/// <summary>
/// If the target agent needs some required fields but the
/// </summary>
/// <returns></returns>
public bool HasMissingRequiredField(RoleDialogModel message, out string agentId)
{
var args = JsonSerializer.Deserialize<RoutingArgs>(message.FunctionArgs);
var routing = _services.GetRequiredService<IRoutingService>();
var routingRules = routing.GetRulesByAgentName(args.AgentName);
if (routingRules == null || !routingRules.Any())
{
agentId = message.CurrentAgentId;
return false;
}
agentId = routingRules.First().AgentId;
// Add routed agent
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "route_to", agentId);
// Check required fields
var root = JsonSerializer.Deserialize<JsonElement>(message.FunctionArgs);
var missingFields = new List<string>();
foreach (var field in routingRules.Where(x => x.Required).Select(x => x.Field))
{
if (!root.EnumerateObject().Any(x => x.Name == field))
{
missingFields.Add(field);
}
else if (root.EnumerateObject().Any(x => x.Name == field) &&
string.IsNullOrEmpty(root.EnumerateObject().FirstOrDefault(x => x.Name == field).Value.ToString()))
{
missingFields.Add(field);
}
}
// Check if states contains the field according conversation context.
var states = _services.GetRequiredService<IConversationStateService>();
foreach (var field in missingFields.ToList())
{
if (!string.IsNullOrEmpty(states.GetState(field)))
{
var value = states.GetState(field);
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, field, value);
missingFields.Remove(field);
}
}
if (missingFields.Any())
{
// Add field to args
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "missing_fields", missingFields);
message.Content = $"missing some information: {string.Join(", ", missingFields)}";
// Handle redirect
var routingRule = routingRules.FirstOrDefault(x => missingFields.Contains(x.Field));
if (!string.IsNullOrEmpty(routingRule.RedirectTo))
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetAgent(routingRule.RedirectTo);
// Add redirected agent
message.FunctionArgs = AppendPropertyToArgs(message.FunctionArgs, "redirect_to", record.Name);
agentId = routingRule.RedirectTo;
var logger = _services.GetRequiredService<ILogger<RouteToAgentFn>>();
#if DEBUG
Console.WriteLine($"*** Routing redirect to {record.Name.ToUpper()} ***", Color.Yellow);
#else
logger.LogInformation($"*** Routing redirect to {record.Name.ToUpper()} ***");
#endif
}
else
{
// back to router
agentId = message.CurrentAgentId;
}
}
return missingFields.Any();
}
private string AppendPropertyToArgs(string args, string key, string value)
{
return args.Substring(0, args.Length - 1) + $", \"{key}\": \"{value}\"" + "}";
}
private string AppendPropertyToArgs(string args, string key, IEnumerable<string> values)
{
string fields = string.Join(",", values.Select(x => $"\"{x}\""));
return args.Substring(0, args.Length - 1) + $", \"{key}\": [{fields}]" + "}";
}
}

View file

@ -36,6 +36,12 @@ public partial class RoutingService
{
result = await function.Execute(message);
}
catch (JsonException ex)
{
_logger.LogError($"The input does not contain any JSON tokens:\r\n{message.Content}");
message.StopCompletion = true;
message.Content = ex.Message;
}
catch (Exception ex)
{
message.StopCompletion = true;

View file

@ -7,6 +7,11 @@ You're {{router.name}} ({{router.description}}). Follow these steps to handle us
6. Please do not make up any parameters when there is no exact information available, leave it blank.
7. Response must be in JSON format.
[REQUIREMENTS]
{% for requirement in routing_requirements %}
# {{ requirement }}
{% endfor %}
[FUNCTIONS]
{% for handler in routing_handlers %}
# {{ handler.description}}

View file

@ -11,6 +11,8 @@ using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Microsoft.IdentityModel.JsonWebTokens;
using BotSharp.OpenAPI.BackgroundServices;
using BotSharp.Abstraction.Settings;
using BotSharp.Abstraction.Google.Settings;
namespace BotSharp.OpenAPI;
@ -32,6 +34,12 @@ public static class BotSharpOpenApiExtensions
services.AddScoped<IUserIdentity, UserIdentity>();
services.AddHostedService<ConversationTimeoutService>();
services.AddScoped(provider =>
{
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<GoogleApiSettings>("GoogleApi");
});
// Add bearer authentication
var schema = "MIXED_SCHEME";
var builder = services.AddAuthentication(options =>

View file

@ -0,0 +1,50 @@
using BotSharp.Abstraction.Google.Models;
using BotSharp.Abstraction.Google.Settings;
using BotSharp.Abstraction.Options;
namespace BotSharp.OpenAPI.Controllers;
[Authorize]
[ApiController]
public class AddressController : ControllerBase
{
private readonly IServiceProvider _services;
private readonly BotSharpOptions _options;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public AddressController(IServiceProvider services,
IHttpClientFactory httpClientFactory,
BotSharpOptions options)
{
_services = services;
_options = options;
_httpClientFactory = httpClientFactory;
}
[HttpGet("/address/options")]
public async Task<GoogleAddressResult> GetAddressOptions([FromQuery] string address)
{
var result = new GoogleAddressResult();
try
{
var settings = _services.GetRequiredService<GoogleApiSettings>();
using var client = _httpClientFactory.CreateClient();
var url = $"{settings.Endpoint}?key={settings.ApiKey}&" +
$"components={settings.Components}&" +
$"language={settings.Language}&" +
$"address={address}";
var response = await client.GetAsync(url);
var responseStr = await response.Content.ReadAsStringAsync();
result = JsonSerializer.Deserialize<GoogleAddressResult>(responseStr, _options.JsonSerializerOptions);
}
catch (Exception ex)
{
_logger.LogError($"Error when calling google geocoding api... ${ex.Message}");
}
return result;
}
}

View file

@ -267,7 +267,7 @@ public class ChatCompletionProvider : IChatCompletion
var samplingFactor = float.Parse(state.GetState("sampling_factor", "0.0"));
chatCompletionsOptions.Temperature = temperature;
chatCompletionsOptions.NucleusSamplingFactor = samplingFactor;
chatCompletionsOptions.MaxTokens = int.Parse(state.GetState("max_tokens", "256"));
chatCompletionsOptions.MaxTokens = int.Parse(state.GetState("max_tokens", "1024"));
// chatCompletionsOptions.FrequencyPenalty = 0;
// chatCompletionsOptions.PresencePenalty = 0;

View file

@ -215,6 +215,13 @@
"ModelVersion": "V3_5"
},
"GoogleApi": {
"Endpoint": "https://maps.googleapis.com/maps/api/geocode/json",
"ApiKey": "",
"Components": "country=US|country=CA",
"Language": "en"
},
"PluginLoader": {
"Assemblies": [
"BotSharp.Core",