diff --git a/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleAddressResult.cs b/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleAddressResult.cs new file mode 100644 index 00000000..a327102d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Google/Models/GoogleAddressResult.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Abstraction.Google.Models; + +public class GoogleAddressResult +{ + public IList Results { get; set; } = new List(); + public string Status { get; set; } +} + + +public class GoogleAddress +{ + [JsonPropertyName("formatted_address")] + public string FormatedAddress { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs new file mode 100644 index 00000000..d43ec58b --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Google/Settings/GoogleApiSettings.cs @@ -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; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs index fbc2f9d6..4c92a3fb 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/ButtonTemplateMessage.cs @@ -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; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs index 32d6a4f6..db581f78 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/GenericTemplateMessage.cs @@ -16,6 +16,9 @@ public class GenericTemplateMessage : IRichMessage, ITemplateMessage [JsonPropertyName("elements")] public List Elements { get; set; } = new List(); + [JsonPropertyName("is_horizontal")] + public bool IsHorizontal { get; set; } + [JsonPropertyName("element_type")] public string ElementType => typeof(T).Name; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs index b838ba03..5e72f1b1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Messaging/Models/RichContent/Template/MultiSelectTemplateMessage.cs @@ -11,7 +11,12 @@ public class MultiSelectTemplateMessage : IRichMessage, ITemplateMessage [JsonPropertyName("template_type")] public string TemplateType => TemplateTypeEnum.MultiSelect; + + [JsonPropertyName("options")] public List Options { get; set; } = new List(); + + [JsonPropertyName("is_horizontal")] + public bool IsHorizontal { get; set; } } public class OptionElement diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index 74845e1f..48daad61 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -43,4 +43,6 @@ public interface IRoutingService Task InstructDirect(Agent agent, RoleDialogModel message); Task GetConversationContent(List dialogs, int maxDialogCount = 50); + + bool HasMissingRequiredField(RoleDialogModel message, out string agentId); } diff --git a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs index 44177b09..2b56281c 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpCoreExtensions.cs @@ -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; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs index 476611d9..d2f478e8 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Functions/RouteToAgentFn.cs @@ -1,13 +1,12 @@ using BotSharp.Abstraction.Functions; using BotSharp.Abstraction.Routing.Models; -using System.Drawing; namespace BotSharp.Core.Routing; /// /// Router calls this function to set the Active Agent according to the context /// -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(); + 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; } - - /// - /// If the target agent needs some required fields but the - /// - /// - private bool HasMissingRequiredField(RoleDialogModel message, out string agentId) - { - var args = JsonSerializer.Deserialize(message.FunctionArgs); - var routing = _services.GetRequiredService(); - - 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(message.FunctionArgs); - var missingFields = new List(); - 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(); - 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(); - 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>(); -#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 values) - { - string fields = string.Join(",", values.Select(x => $"\"{x}\"")); - return args.Substring(0, args.Length - 1) + $", \"{key}\": [{fields}]" + "}"; - } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs index 462ee076..1b8cb284 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Handlers/RouteToAgentRoutingHandler.cs @@ -10,7 +10,7 @@ public class RouteToAgentRoutingHandler : RoutingHandlerBase, IRoutingHandler public List Parameters => new List { - 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 }, diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index 96cd95e6..e9abdc04 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -105,6 +105,31 @@ public class RoutingContext : IRoutingContext HookEmitter.Emit(_services, async hook => await hook.OnAgentDequeued(agentId, currentAgentId, reason: reason) ).Wait(); + + // Run the routing rule + var agency = _services.GetRequiredService(); + 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(); + 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; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs new file mode 100644 index 00000000..20d75a95 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.HasMissingRequiredField.cs @@ -0,0 +1,100 @@ +using BotSharp.Abstraction.Routing.Models; +using System.Drawing; + +namespace BotSharp.Core.Routing; + +public partial class RoutingService +{ + /// + /// If the target agent needs some required fields but the + /// + /// + public bool HasMissingRequiredField(RoleDialogModel message, out string agentId) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs); + var routing = _services.GetRequiredService(); + + 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(message.FunctionArgs); + var missingFields = new List(); + 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(); + 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(); + 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>(); +#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 values) + { + string fields = string.Join(",", values.Select(x => $"\"{x}\"")); + return args.Substring(0, args.Length - 1) + $", \"{key}\": [{fields}]" + "}"; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 77659d69..690308eb 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -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; diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid index 3a313023..990052cf 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/instruction.liquid @@ -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}} diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 0434976e..63a9bc92 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -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(); services.AddHostedService(); + services.AddScoped(provider => + { + var settingService = provider.GetRequiredService(); + return settingService.Bind("GoogleApi"); + }); + // Add bearer authentication var schema = "MIXED_SCHEME"; var builder = services.AddAuthentication(options => diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AddressController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AddressController.cs new file mode 100644 index 00000000..755b8232 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AddressController.cs @@ -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 GetAddressOptions([FromQuery] string address) + { + var result = new GoogleAddressResult(); + + try + { + var settings = _services.GetRequiredService(); + 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(responseStr, _options.JsonSerializerOptions); + } + catch (Exception ex) + { + _logger.LogError($"Error when calling google geocoding api... ${ex.Message}"); + } + + return result; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index ba5a393a..cc19b7b1 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -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; diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 46340bc3..ed24f1db 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -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",