diff --git a/Directory.Build.props b/Directory.Build.props index 6940e0bc..86eafbe7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ 10.0 ..\..\..\packages - 0.13.0 + 0.14.1 true \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs index 66360468..6e5963e8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -5,13 +5,16 @@ namespace BotSharp.Abstraction.Functions.Models; public class FunctionCallFromLlm { [JsonPropertyName("function")] - public string Function { get; set; } + public string Function { get; set; } = string.Empty; + + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; [JsonPropertyName("parameters")] - public RetrievalArgs Parameters { get; set; } + public RetrievalArgs Parameters { get; set; } = new RetrievalArgs(); public override string ToString() { - return $"{Function} {Parameters}"; + return $"{Function} ({Reason}) {Parameters}"; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs index 816b6324..51f2f777 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs @@ -10,14 +10,18 @@ public class RetrievalArgs : RoutingArgs [JsonPropertyName("answer")] public string Answer { get; set; } - [JsonPropertyName("reason")] - public string Reason { get; set; } - [JsonPropertyName("args")] - public JsonDocument Arguments { get; set; } + public JsonDocument Arguments { get; set; } = JsonDocument.Parse("{}"); public override string ToString() { - return $"[{AgentName}, {Reason}]: ({JsonSerializer.Serialize(Arguments)}) {Question}"; + if (string.IsNullOrEmpty(Answer)) + { + return $"[{AgentName}]: ({JsonSerializer.Serialize(Arguments)}) {Question}"; + } + else + { + return $"[{AgentName}]: ({JsonSerializer.Serialize(Arguments)}) {Question} => {Answer}"; + } } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs index e70d787d..474e26da 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs @@ -5,7 +5,7 @@ namespace BotSharp.Abstraction.Routing.Models; public class RoutingArgs { [JsonPropertyName("agent_name")] - public string AgentName { get; set; } + public string AgentName { get; set; } = string.Empty; public override string ToString() { diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index 02c5a515..89e1c57f 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -56,7 +56,6 @@ public static class BotSharpServiceCollectionExtensions // Register function callback services.AddScoped(); - services.AddScoped(); services.AddScoped(); if (myDatabaseSettings.Default == "FileRepository") diff --git a/src/Infrastructure/BotSharp.Core/Routing/Prompts/reasoning_functions.liquid b/src/Infrastructure/BotSharp.Core/Routing/Prompts/reasoning_functions.liquid index 44b29104..0831261b 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Prompts/reasoning_functions.liquid +++ b/src/Infrastructure/BotSharp.Core/Routing/Prompts/reasoning_functions.liquid @@ -1,22 +1,26 @@ -# retrieve_data_from_agent +* retrieve_data_from_agent Retrieve data from appropriate agent. Parameters: - 1. agent_name: the name of the agent; - 2. question: the question you will ask the agent to get the necessary data - 3. args: required parameters extracted from question and hand over to the next agent. The args should be in JSON format. +1. agent_name: the name of the agent; +2. question: the question you will ask the agent to get the necessary data; +3. reason: why retrieve data; +4. args: required parameters extracted from question and hand over to the next agent. The args should be in JSON format; -# continue_execute_task +* continue_execute_task Continue to execute user's request without further information retrival. Parameters: - 1. agent_name: the name of the agent; - 2. args: required parameters extracted from question. +1. agent_name: the name of the agent; +2. args: required parameters extracted from question; +3. reason: why continue to execute current task; -# interrupt_task_execution -Can't continue user's request becauase the requirements are not met or you have already known the answer. +* interrupt_task_execution +Can't continue user's request becauase the requirements are not met. Parameters: - 1. reason: the reason why the request is interrupted; +1. reason: the reason why the request is interrupted; +2. answer: the content response to user; -# response_to_user +* response_to_user You have already known the answer according the dialogs. Parameters: - 1. answer: the answer of user's question; \ No newline at end of file +1. answer: the response of user's request; +2. reason: why response to user; diff --git a/src/Infrastructure/BotSharp.Core/Routing/Prompts/router_prompt.liquid b/src/Infrastructure/BotSharp.Core/Routing/Prompts/router_prompt.liquid index 4ff1d7df..941e7bdd 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Prompts/router_prompt.liquid +++ b/src/Infrastructure/BotSharp.Core/Routing/Prompts/router_prompt.liquid @@ -1,32 +1,38 @@ -You're a Agent Router with reasoning, you can dispatch request to different agent to complete the task. +You're a Router with reasoning, you can dispatch request to different agent to complete the task. +### Agents: {% for agent in routing_records %} -# Agent: {{ agent.name }} +* {{ agent.name }} {{ agent.description }} {% if agent.required_fields != empty -%}Required information: {{ agent.required_fields }}.{%- endif %} {% endfor %} -### Function instructions -# route_to_agent +### Functions +{% if enable_reasoning == false -%} +* route_to_agent Route request to appropriate agent. Parameters: - 1. agent_name: the name of the agent; - 2. reason: why route to this agent; +1. agent_name: the name of the agent; +2. reason: why route to this agent; +3. args: parameters extracted from context; +{%- endif %} -# task_end +* task_end Call this function when current task is completed. Parameters: - 1. abandoned_arguments: the arguments next task can't reuse; +1. abandoned_arguments: the arguments next task can't reuse; -# conversation_end +* conversation_end Call this function when user wants to end this conversation or all tasks have been completed. -# transfer_to_csr +* transfer_to_csr Reach out to a real customer representative to help. {{ reasoning_functions }} ### Your response must meet below requirements strictly +{% if enable_reasoning == false %} * If you can find an appropriate Agent, you must call function route_to_agent with required arguments. +{% endif %} -### Below are the dialogs between user and different agents: \ No newline at end of file +### Conversation context: \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index f4337723..0feb3cb1 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -27,24 +27,24 @@ public class RoutingService : IRoutingService _logger = logger; } - public async Task Enter(Agent agent, List whileDialogs) + public async Task Enter(Agent router, List whileDialogs) { _dialogs = new List(); RoleDialogModel result = new RoleDialogModel(AgentRole.Assistant, "not handled"); foreach (var dialog in whileDialogs.TakeLast(20)) { - agent.Instruction += $"\r\n{dialog.Role}: {dialog.Content}"; + router.Instruction += $"\r\n{dialog.Role}: {dialog.Content}"; } - var inst = await GetNextInstructionFromReasoner(agent); + var inst = await GetNextInstructionFromReasoner($"What's the next step to make user's original goal?", router); int loopCount = 0; while (loopCount < 3) { loopCount++; if (inst.Function == "continue_execute_task") { - var router = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); var db = _services.GetRequiredService(); var record = db.Agents.First(x => x.Name.ToLower() == inst.Parameters.AgentName.ToLower()); @@ -73,7 +73,7 @@ public class RoutingService : IRoutingService } else if (inst.Function == "interrupt_task_execution") { - result = new RoleDialogModel(AgentRole.User, inst.Parameters.Reason) + result = new RoleDialogModel(AgentRole.User, inst.Reason) { FunctionName = inst.Function }; @@ -98,32 +98,39 @@ public class RoutingService : IRoutingService }); inst.Parameters.Answer = response.Content; - response.Content += $"\r\nDo you want to continue current task?"; - - _dialogs.Add(new RoleDialogModel(AgentRole.Function, $"{record.Name}: {response.Content}") + + _dialogs.Add(new RoleDialogModel(AgentRole.Assistant, inst.Parameters.Question) { - FunctionName = inst.Function, - FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), - ExecutionResult = response.Content, CurrentAgentId = record.Id }); - agent.Instruction += $"\r\n{record.Name}: {response.Content}"; + router.Instruction += $"\r\n{AgentRole.Assistant}: {inst.Parameters.Question}"; + + _dialogs.Add(new RoleDialogModel(AgentRole.Function, inst.Parameters.Answer) + { + FunctionName = inst.Function, + FunctionArgs = JsonSerializer.Serialize(inst.Parameters.Arguments), + ExecutionResult = inst.Parameters.Answer, + ExecutionData = response.ExecutionData, + CurrentAgentId = record.Id + }); + + router.Instruction += $"\r\n{AgentRole.Function}: {response.Content}"; // Got the response from agent, then send to reasoner again to make the decision - inst = await GetNextInstructionFromReasoner(agent); + inst = await GetNextInstructionFromReasoner($"What's the next step based on user's original goal and function result?", router); } } return result; } - private async Task GetNextInstructionFromReasoner(Agent reasoner) + private async Task GetNextInstructionFromReasoner(string prompt, Agent reasoner) { - var responseFormat = "{\"function\": \"\", \"parameters\": {\"agent_name\": \"\", \"reason\":\"\", \"args\":{}}"; + var responseFormat = JsonSerializer.Serialize(new FunctionCallFromLlm()); var wholeDialogs = new List { - new RoleDialogModel(AgentRole.System, $"What's the next step? Response in JSON format {responseFormat}.") + new RoleDialogModel(AgentRole.User, $"{prompt} Response in JSON format {responseFormat}") }; var chatCompletion = CompletionProvider.GetChatCompletion(_services, @@ -143,6 +150,7 @@ public class RoutingService : IRoutingService } args.Function = args.Function.Split('.').Last(); + args.Parameters.AgentName = args.Parameters.AgentName.Split(':').Last().Trim(); _logger.LogInformation($"*** Next Instruction *** {args}"); @@ -218,7 +226,8 @@ public class RoutingService : IRoutingService var dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Routing", "Prompts"); var template = File.ReadAllText(Path.Combine(dir, "router_prompt.liquid")); - + + dict["enable_reasoning"] = _settings.EnableReasoning; if (_settings.EnableReasoning) { dict["reasoning_functions"] = File.ReadAllText(Path.Combine(dir, "reasoning_functions.liquid")); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs b/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs deleted file mode 100644 index b13eb5bb..00000000 --- a/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs +++ /dev/null @@ -1,179 +0,0 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; -using BotSharp.Abstraction.Routing.Models; - -namespace BotSharp.Core.Routing; - -/// -/// Simulate the dialogue between different agents. -/// -public class Simulator -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private List _dialogs; - public List Dialogs => _dialogs; - - public Simulator(IServiceProvider services, ILogger logger) - { - _services = services; - _logger = logger; - } - - public async Task Enter(Agent agent, List whileDialogs) - { - _dialogs = new List(); - - foreach (var dialog in whileDialogs.TakeLast(10)) - { - agent.Instruction += $"\r\n{dialog.Role}: {dialog.Content}"; - } - - var response = await SendMessageToReasoner(agent); - if (response.Role == AgentRole.Function) - { - - } - - var args = JsonSerializer.Deserialize(response.Content); - response.FunctionName = args.Function; - - if (args.Function == "continue_execute_task") - { - response.FunctionArgs = JsonSerializer.Serialize(args.Parameters.Arguments); - - var router = _services.GetRequiredService(); - var db = _services.GetRequiredService(); - var record = db.Agents.First(x => x.Name.ToLower() == args.Parameters.AgentName.ToLower()); - response.CurrentAgentId = record.Id; - } - else if (args.Function == "interrupt_task_execution") - { - response.Content = args.Parameters.Reason; - response.ExecutionResult = args.Parameters.Reason; - } - else if (args.Function == "response_to_user") - { - response.Content = args.Parameters.Answer; - response.ExecutionResult = args.Parameters.Answer; - } - - return response; - } - - private async Task SendMessageToReasoner(Agent reasoner) - { - var wholeDialogs = new List - { - new RoleDialogModel(AgentRole.User, @"What's the next step? Response in JSON format with ""function"" and ""parameters"".") - }; - - var chatCompletion = CompletionProvider.GetChatCompletion(_services); - - RoleDialogModel response = null; - await chatCompletion.GetChatCompletionsAsync(reasoner, wholeDialogs, async msg - => response = msg, fn - => Task.CompletedTask); - - var args = JsonSerializer.Deserialize(response.Content); - - if (args.Parameters.Arguments != null) - { - SaveStateByArgs(args.Parameters.Arguments); - } - else if (args.Function == "response_to_user") - { - return response; - } - - if (args.Function == "route_to_agent") - { - var function = _services.GetServices().FirstOrDefault(x => x.Name == args.Function); - var message = new RoleDialogModel(AgentRole.Function, args.Parameters.Question) - { - FunctionName = args.Function, - FunctionArgs = JsonSerializer.Serialize(new RoutingArgs - { - AgentName = args.Parameters.AgentName - }), - }; - var ret = await function.Execute(message); - if (ret) - { - return message; - } - } - - // Retrieve information from specific agent - var router = _services.GetRequiredService(); - var db = _services.GetRequiredService(); - var record = db.Agents.First(x => x.Name.ToLower() == args.Parameters.AgentName.ToLower()); - response = await SendMessageToAgent(record.Id, new List - { - new RoleDialogModel(AgentRole.User, args.Parameters.Question) - }); - - _dialogs.Add(new RoleDialogModel(AgentRole.Function, $"{record.Name}: {response.Content}") - { - FunctionName = args.Function, - FunctionArgs = JsonSerializer.Serialize(args.Parameters.Arguments), - ExecutionResult = response.Content - }); - - reasoner.Instruction += $"\r\n{record.Name}: {response.Content}"; - // Got the response from agent, then send to reasoner again to make the decision - await chatCompletion.GetChatCompletionsAsync(reasoner, wholeDialogs, async msg - => response = msg, fn - => Task.CompletedTask); - - return response; - } - - private async Task SendMessageToAgent(string agentId, List wholeDialogs) - { - var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(agentId); - - var chatCompletion = CompletionProvider.GetChatCompletion(_services); - - RoleDialogModel response = null; - await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg - => response = msg, async fn - => - { - // execute function - // Save states - SaveStateByArgs(JsonSerializer.Deserialize(fn.FunctionArgs)); - - var conversationService = _services.GetRequiredService(); - // Call functions - await conversationService.CallFunctions(fn); - - response = fn; - response.Content = fn.ExecutionResult; - }); - return response; - } - - private void SaveStateByArgs(JsonDocument args) - { - if (args == null) - { - return; - } - - var stateService = _services.GetRequiredService(); - if (args.RootElement is JsonElement root) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - if (!string.IsNullOrEmpty(property.Value.ToString())) - { - stateService.SetState(property.Name, property.Value); - } - } - } - } -} diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index 5434d586..acc839c4 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -237,6 +237,8 @@ public class ChatCompletionProvider : IChatCompletion var samplingFactor = float.Parse(state.GetState("sampling_factor", "0.5")); chatCompletionsOptions.Temperature = temperature; chatCompletionsOptions.NucleusSamplingFactor = samplingFactor; + chatCompletionsOptions.FrequencyPenalty = 0; + chatCompletionsOptions.PresencePenalty = 0; var convSetting = _services.GetRequiredService(); if (convSetting.ShowVerboseLog) diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index caa2bb49..cb89eecd 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -15,7 +15,8 @@ "Router": { "RouterId": "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a", - "EnableReasoning": false + "EnableReasoning": false, + "Model": "gpt-3.5" }, "Agent": { diff --git a/src/WebStarter/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions.json b/src/WebStarter/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions.json index 17065598..fbcb82dd 100644 --- a/src/WebStarter/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions.json +++ b/src/WebStarter/data/agents/b284db86-e9c2-4c25-a59e-4649797dd130/functions.json @@ -1,7 +1,7 @@ [ { - "name": "get_delivery_time", - "description": "get order delivery remaining time", + "name": "get_order_status", + "description": "get order status like delivery remaining time", "parameters": { "type": "object", "properties": { diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetDeliveryTimeFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetDeliveryTimeFn.cs deleted file mode 100644 index db3179af..00000000 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/GetDeliveryTimeFn.cs +++ /dev/null @@ -1,14 +0,0 @@ -using BotSharp.Abstraction.Conversations.Models; - -namespace BotSharp.Plugin.PizzaBot.Functions; - -public class GetDeliveryTimeFn : IFunctionCallback -{ - public string Name => "get_delivery_time"; - - public async Task Execute(RoleDialogModel message) - { - message.ExecutionResult = "15 minutes remaining"; - return true; - } -} diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetOrderStatusFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetOrderStatusFn.cs new file mode 100644 index 00000000..1cadc455 --- /dev/null +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/GetOrderStatusFn.cs @@ -0,0 +1,19 @@ +using BotSharp.Abstraction.Conversations.Models; + +namespace BotSharp.Plugin.PizzaBot.Functions; + +public class GetOrderStatusFn : IFunctionCallback +{ + public string Name => "get_order_status"; + + public async Task Execute(RoleDialogModel message) + { + message.ExecutionResult = "ready to deliver, will arrived in about 15 minutes."; + message.ExecutionData = new + { + Status = "Ready to deliver", + EstimatedTime = "15 minuts" + }; + return true; + } +} diff --git a/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs b/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs index bc106e70..087b6d8a 100644 --- a/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs +++ b/tests/BotSharp.Plugin.PizzaBot/PizzaBotPlugin.cs @@ -12,7 +12,7 @@ public class PizzaBotPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); // Register hooks