From 6f5cf2fcae9711259dfac32105aab70290482097 Mon Sep 17 00:00:00 2001 From: hchen2020 <101423@smsassist.com> Date: Sun, 27 Aug 2023 22:50:10 -0500 Subject: [PATCH] Draft of Reasoning --- .../Agents/IAgentRouting.cs | 5 +- .../Agents/Settings/AgentSettings.cs | 7 +- .../Conversations/Models/RoleDialogModel.cs | 2 +- .../Settings/ConversationSetting.cs | 2 + .../Functions/Models/FunctionCallFromLlm.cs | 14 + .../Routing/Models/RetrievalArgs.cs | 16 ++ .../{Agents => Routing}/Models/RoutingArgs.cs | 2 +- .../Models/RoutingRecord.cs | 2 +- .../Routing/Settings/GPT4Settings.cs | 8 + .../Agents/Services/AgentRouter.cs | 53 ---- .../BotSharp.Core/BotSharp.Core.csproj | 4 - .../BotSharpServiceCollectionExtensions.cs | 14 +- ...vice.GetChatCompletionsAsyncRecursively.cs | 5 +- .../ConversationService.SendMessage.cs | 43 +++- .../Services/ConversationStorage.cs | 1 + .../BotSharp.Core/Functions/RouteToAgentFn.cs | 5 +- .../BotSharp.Core/Hooks/ReasoningHook.cs | 14 + .../Hooks/{AgentHook.cs => RoutingHook.cs} | 4 +- .../BotSharp.Core/Routing/Reasoner.cs | 12 + .../BotSharp.Core/Routing/Router.cs | 43 ++++ .../BotSharp.Core/Routing/Simulator.cs | 133 ++++++++++ .../Templating/TemplateRender.cs | 3 +- .../AzureOpenAiPlugin.cs | 1 + .../Providers/GPT4CompletionProvider.cs | 240 ++++++++++++++++++ .../Settings/AzureOpenAiSettings.cs | 4 + 25 files changed, 560 insertions(+), 77 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs rename src/Infrastructure/BotSharp.Abstraction/{Agents => Routing}/Models/RoutingArgs.cs (82%) rename src/Infrastructure/BotSharp.Abstraction/{Agents => Routing}/Models/RoutingRecord.cs (92%) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Routing/Settings/GPT4Settings.cs delete mode 100644 src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs create mode 100644 src/Infrastructure/BotSharp.Core/Hooks/ReasoningHook.cs rename src/Infrastructure/BotSharp.Core/Hooks/{AgentHook.cs => RoutingHook.cs} (77%) create mode 100644 src/Infrastructure/BotSharp.Core/Routing/Reasoner.cs create mode 100644 src/Infrastructure/BotSharp.Core/Routing/Router.cs create mode 100644 src/Infrastructure/BotSharp.Core/Routing/Simulator.cs create mode 100644 src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/GPT4CompletionProvider.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs index 4df491db..d1086c26 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentRouting.cs @@ -1,8 +1,11 @@ +using BotSharp.Abstraction.Routing.Models; + namespace BotSharp.Abstraction.Agents; public interface IAgentRouting { + string AgentId { get; } Task LoadRouter(); - Task LoadCurrentAgent(); RoutingRecord[] GetRoutingRecords(); + RoutingRecord GetRecordByName(string name); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs index 84ff0274..60307439 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Settings/AgentSettings.cs @@ -6,7 +6,12 @@ public class AgentSettings /// Router Agent Id /// public string RouterId { get; set; } + + /// + /// Reasoner Agent Id + /// + public string ReasonerId { get; set; } + public string DataDir { get; set; } public string TemplateFormat { get; set; } - public int MaxRecursiveDepth { get; set; } = 3; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index b681c2c3..e80c9427 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -45,7 +45,7 @@ public class RoleDialogModel { if (Role == AgentRole.Function) { - return $"{Role}: {FunctionName}"; + return $"{Role}: {FunctionName} => {ExecutionResult}"; } else { diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/ConversationSetting.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/ConversationSetting.cs index 80897657..c9836d4b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/ConversationSetting.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Settings/ConversationSetting.cs @@ -6,4 +6,6 @@ public class ConversationSetting public string ChatCompletion { get; set; } public bool EnableKnowledgeBase { get; set; } public bool ShowVerboseLog { get; set; } + public int MaxRecursiveDepth { get; set; } = 3; + public bool EnableReasoning { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs new file mode 100644 index 00000000..5d9b9881 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Functions/Models/FunctionCallFromLlm.cs @@ -0,0 +1,14 @@ +using BotSharp.Abstraction.Routing.Models; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BotSharp.Abstraction.Functions.Models; + +public class FunctionCallFromLlm +{ + [JsonPropertyName("function")] + public string Function { get; set; } + + [JsonPropertyName("parameters")] + public RetrievalArgs Parameters { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs new file mode 100644 index 00000000..835cda46 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RetrievalArgs.cs @@ -0,0 +1,16 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BotSharp.Abstraction.Routing.Models; + +public class RetrievalArgs : RoutingArgs +{ + [JsonPropertyName("question")] + public string Question { get; set; } + + [JsonPropertyName("reason")] + public string Reason { get; set; } + + [JsonPropertyName("args")] + public JsonDocument Arguments { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs similarity index 82% rename from src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingArgs.cs rename to src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs index e02b1c7b..e70d787d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingArgs.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace BotSharp.Abstraction.Agents.Models; +namespace BotSharp.Abstraction.Routing.Models; public class RoutingArgs { diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingRecord.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRecord.cs similarity index 92% rename from src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingRecord.cs rename to src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRecord.cs index 3d8e1868..f0d6d742 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/RoutingRecord.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Models/RoutingRecord.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace BotSharp.Abstraction.Agents.Models; +namespace BotSharp.Abstraction.Routing.Models; public class RoutingRecord { diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/Settings/GPT4Settings.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Settings/GPT4Settings.cs new file mode 100644 index 00000000..21925c1c --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Settings/GPT4Settings.cs @@ -0,0 +1,8 @@ +namespace BotSharp.Abstraction.Routing.Settings; + +public class GPT4Settings +{ + public string ApiKey { get; set; } + public string Endpoint { get; set; } + public string DeploymentModel { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs deleted file mode 100644 index 0aed1531..00000000 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using BotSharp.Abstraction.Agents.Models; -using System.IO; - -namespace BotSharp.Core.Agents.Services; - -public class AgentRouter : IAgentRouting -{ - private readonly IServiceProvider _services; - private readonly ILogger _logger; - private readonly AgentSettings _settings; - - public AgentRouter(IServiceProvider services, - ILogger logger, - AgentSettings settings) - { - _services = services; - _logger = logger; - _settings = settings; - } - - public async Task LoadRouter() - { - var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(_settings.RouterId); - return agent; - } - - public async Task LoadCurrentAgent() - { - // Load current agent from state - var state = _services.GetRequiredService(); - var currentAgentId = state.GetState("agent_id"); - if (string.IsNullOrEmpty(currentAgentId)) - { - currentAgentId = _settings.RouterId; - } - var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(currentAgentId); - - // Set agent and trigger state changed - state.SetState("agent_id", currentAgentId); - - return agent; - } - - public RoutingRecord[] GetRoutingRecords() - { - var agentSettings = _services.GetRequiredService(); - var dbSettings = _services.GetRequiredService(); - var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir, agentSettings.RouterId, "route.json"); - return JsonSerializer.Deserialize(File.ReadAllText(filePath)); - } -} diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 20b036e4..ab77723b 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -82,8 +82,4 @@ - - - - diff --git a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs index 1b38659c..b200c729 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/BotSharpServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Functions; using BotSharp.Core.Functions; using BotSharp.Core.Hooks; +using BotSharp.Core.Routing; using BotSharp.Core.Templating; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; @@ -41,13 +43,21 @@ public static class BotSharpServiceCollectionExtensions services.AddSingleton(); // Register router - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(p => + { + var setting = p.GetRequiredService(); + return setting.EnableReasoning ? p.GetRequiredService() : p.GetRequiredService(); + }); // Register function callback services.AddScoped(); // Register Hooks - services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); return services; } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs index 40856809..4e5a0d23 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs @@ -13,13 +13,12 @@ public partial class ConversationService string conversationId, Agent agent, List wholeDialogs, - int maxRecursiveDepth, Func onMessageReceived, Func onFunctionExecuting, Func onFunctionExecuted) { currentRecursiveDepth++; - if (currentRecursiveDepth > maxRecursiveDepth) + if (currentRecursiveDepth > _settings.MaxRecursiveDepth) { _logger.LogWarning($"Exceeded max recursive depth."); @@ -65,7 +64,6 @@ public partial class ConversationService fn.Content = fn.FunctionArgs.Replace("\r", " ").Replace("\n", " ").Trim() + " => " + fn.ExecutionResult; // Agent has been transferred - var agentSettings = _services.GetRequiredService(); if (fn.CurrentAgentId != preAgentId) { var agentService = _services.GetRequiredService(); @@ -83,7 +81,6 @@ public partial class ConversationService conversationId, agent, wholeDialogs, - maxRecursiveDepth, onMessageReceived, onFunctionExecuting, onFunctionExecuted); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 8d53971a..c05406a5 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -1,5 +1,8 @@ +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.MLTasks; +using BotSharp.Core.Routing; namespace BotSharp.Core.Conversations.Services; @@ -31,7 +34,7 @@ public partial class ConversationService stateService.SetState("channel", lastDialog.Channel); var router = _services.GetRequiredService(); - var agent = await router.LoadRouter(); + Agent agent = await router.LoadRouter(); _logger.LogInformation($"[{agent.Name}] {lastDialog.Role}: {lastDialog.Content}"); @@ -65,14 +68,42 @@ public partial class ConversationService await hook.BeforeCompletion(); } - var agentSettings = _services.GetRequiredService(); + // reasoning + if (_settings.EnableReasoning) + { + var simulator = _services.GetRequiredService(); + var reasonedContext = await simulator.Enter(agent, wholeDialogs); + + if (reasonedContext.FunctionName == "interrupt_task_execution") + { + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, reasonedContext.Content) + { + CurrentAgentId = agent.Id, + Channel = lastDialog.Channel + }, onMessageReceived); + return true; + } + else if (reasonedContext.FunctionName == "continue_execute_task") + { + if (reasonedContext.CurrentAgentId != agent.Id) + { + var agentService = _services.GetRequiredService(); + agent = await agentService.LoadAgent(reasonedContext.CurrentAgentId); + } + } + + simulator.Dialogs.ForEach(x => + { + wholeDialogs.Add(x); + _storage.Append(conversationId, agent.Id, x); + }); + } var chatCompletion = GetChatCompletion(); var result = await GetChatCompletionsAsyncRecursively(chatCompletion, conversationId, agent, wholeDialogs, - agentSettings.MaxRecursiveDepth, onMessageReceived, onFunctionExecuting, onFunctionExecuted); @@ -101,4 +132,10 @@ public partial class ConversationService var completions = _services.GetServices(); return completions.FirstOrDefault(x => x.GetType().FullName.EndsWith(_settings.ChatCompletion)); } + + public IChatCompletion GetGpt4ChatCompletion() + { + var completions = _services.GetServices(); + return completions.FirstOrDefault(x => x.GetType().FullName.EndsWith("GPT4CompletionProvider")); + } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 8e0b08dd..7525532b 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -72,6 +72,7 @@ public class ConversationStorage : IConversationStorage CurrentAgentId = currentAgentId, FunctionName = funcName, FunctionArgs = funcArgs, + ExecutionResult = text, CreatedAt = createdAt }); } diff --git a/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs b/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs index 3834b035..4991dc99 100644 --- a/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs +++ b/src/Infrastructure/BotSharp.Core/Functions/RouteToAgentFn.cs @@ -1,6 +1,6 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Routing.Models; using System.IO; namespace BotSharp.Core.Functions; @@ -51,8 +51,7 @@ public class RouteToAgentFn : IFunctionCallback { var args = JsonSerializer.Deserialize(message.FunctionArgs); var router = _services.GetRequiredService(); - var records = router.GetRoutingRecords(); - var routingRule = records.FirstOrDefault(x => x.Name.ToLower() == args.AgentName.ToLower()); + var routingRule = router.GetRecordByName(args.AgentName); if (routingRule == null) { diff --git a/src/Infrastructure/BotSharp.Core/Hooks/ReasoningHook.cs b/src/Infrastructure/BotSharp.Core/Hooks/ReasoningHook.cs new file mode 100644 index 00000000..f754733f --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Hooks/ReasoningHook.cs @@ -0,0 +1,14 @@ +namespace BotSharp.Core.Hooks; + +public class ReasoningHook : AgentHookBase +{ + public ReasoningHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override bool OnInstructionLoaded(string template, Dictionary dict) + { + return true; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Hooks/AgentHook.cs b/src/Infrastructure/BotSharp.Core/Hooks/RoutingHook.cs similarity index 77% rename from src/Infrastructure/BotSharp.Core/Hooks/AgentHook.cs rename to src/Infrastructure/BotSharp.Core/Hooks/RoutingHook.cs index 559445a9..aba0ffc2 100644 --- a/src/Infrastructure/BotSharp.Core/Hooks/AgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Hooks/RoutingHook.cs @@ -1,8 +1,8 @@ namespace BotSharp.Core.Hooks; -public class AgentHook : AgentHookBase +public class RoutingHook : AgentHookBase { - public AgentHook(IServiceProvider services, AgentSettings settings) + public RoutingHook(IServiceProvider services, AgentSettings settings) : base(services, settings) { } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoner.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoner.cs new file mode 100644 index 00000000..c08f83ac --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoner.cs @@ -0,0 +1,12 @@ +namespace BotSharp.Core.Routing; + +public class Reasoner : Router +{ + public override string AgentId => _settings.ReasonerId; + + public Reasoner(IServiceProvider services, + ILogger logger, + AgentSettings settings) : base(services, logger, settings) + { + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Router.cs b/src/Infrastructure/BotSharp.Core/Routing/Router.cs new file mode 100644 index 00000000..594ee734 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Router.cs @@ -0,0 +1,43 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Routing.Models; +using System.IO; +using static Tensorflow.ApiDef.Types; + +namespace BotSharp.Core.Routing; + +public class Router : IAgentRouting +{ + protected readonly IServiceProvider _services; + protected readonly ILogger _logger; + protected readonly AgentSettings _settings; + + public virtual string AgentId => _settings.RouterId; + + public Router(IServiceProvider services, + ILogger logger, + AgentSettings settings) + { + _services = services; + _logger = logger; + _settings = settings; + } + + public virtual async Task LoadRouter() + { + var agentService = _services.GetRequiredService(); + return await agentService.LoadAgent(AgentId); + } + + public RoutingRecord[] GetRoutingRecords() + { + var agentSettings = _services.GetRequiredService(); + var dbSettings = _services.GetRequiredService(); + var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir, agentSettings.RouterId, "route.json"); + return JsonSerializer.Deserialize(File.ReadAllText(filePath)); + } + + public RoutingRecord GetRecordByName(string name) + { + return GetRoutingRecords().First(x => x.Name.ToLower() == name.ToLower()); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs b/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs new file mode 100644 index 00000000..d5c3d935 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Routing/Simulator.cs @@ -0,0 +1,133 @@ +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.MLTasks; + +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); + var args = JsonSerializer.Deserialize(response.Content); + response.FunctionName = args.Function; + response.Content = args.Parameters.Reason; + if (args.Function == "continue_execute_task") + { + response.FunctionArgs = JsonSerializer.Serialize(args.Parameters.Arguments); + + var router = _services.GetRequiredService(); + var record = router.GetRecordByName(args.Parameters.AgentName); + response.CurrentAgentId = record.AgentId; + } + + return response; + } + + private async Task SendMessageToReasoner(Agent reasoner) + { + var wholeDialogs = new List + { + new RoleDialogModel(AgentRole.User, @"What's the next step, your response must be in JSON format with ""function"" and ""parameters"". ") + }; + + var chatCompletion = GetGpt4ChatCompletion(); + + RoleDialogModel response = null; + await chatCompletion.GetChatCompletionsAsync(reasoner, wholeDialogs, async msg + => response = msg, fn + => Task.CompletedTask); + + var args = JsonSerializer.Deserialize(response.Content); + + SaveStateByArgs(args.Parameters.Arguments); + + // Retrieve information from specific agent + var router = _services.GetRequiredService(); + var record = router.GetRecordByName(args.Parameters.AgentName); + response = await SendMessageToAgent(record.AgentId, 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 = GetChatCompletion(); + + RoleDialogModel response = null; + await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg + => response = msg, fn + => Task.CompletedTask); + return response; + } + + public IChatCompletion GetChatCompletion() + { + var completions = _services.GetServices(); + var settings = _services.GetRequiredService(); + return completions.FirstOrDefault(x => x.GetType().FullName.EndsWith(settings.ChatCompletion)); + } + + public IChatCompletion GetGpt4ChatCompletion() + { + var completions = _services.GetServices(); + return completions.FirstOrDefault(x => x.GetType().FullName.EndsWith("GPT4CompletionProvider")); + } + + private void SaveStateByArgs(JsonDocument args) + { + 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.ToString()); + } + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs index ca80356a..97263119 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/TemplateRender.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; using Fluid; using Microsoft.Extensions.Options; @@ -17,7 +18,7 @@ public class TemplateRender : ITemplateRender _services = services; _logger = logger; _options = new TemplateOptions(); - _options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase; + _options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.SnakeCase; _options.MemberAccessStrategy.Register(); } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/AzureOpenAiPlugin.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/AzureOpenAiPlugin.cs index 1232e668..b966d167 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/AzureOpenAiPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/AzureOpenAiPlugin.cs @@ -23,5 +23,6 @@ public class AzureOpenAiPlugin : IBotSharpPlugin services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/GPT4CompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/GPT4CompletionProvider.cs new file mode 100644 index 00000000..8767e7df --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/GPT4CompletionProvider.cs @@ -0,0 +1,240 @@ +using Azure; +using Azure.AI.OpenAI; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Conversations.Settings; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Abstraction.MLTasks; +using BotSharp.Plugin.AzureOpenAI.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace BotSharp.Plugin.AzureOpenAI.Providers; + +public class GPT4CompletionProvider : IChatCompletion +{ + private readonly AzureOpenAiSettings _settings; + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public GPT4CompletionProvider(AzureOpenAiSettings settings, + ILogger logger, + IServiceProvider services) + { + _settings = settings; + _logger = logger; + _services = services; + } + + private OpenAIClient GetClient() + { + var client = new OpenAIClient(new Uri(_settings.GPT4.Endpoint), new AzureKeyCredential(_settings.GPT4.ApiKey)); + return client; + } + + public List GetChatSamples(string sampleText) + { + var samples = new List(); + if (string.IsNullOrEmpty(sampleText)) + { + return samples; + } + + var lines = sampleText.Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + if (string.IsNullOrEmpty(line.Trim())) + { + continue; + } + var role = line.Substring(0, line.IndexOf(' ') - 1).Trim(); + var content = line.Substring(line.IndexOf(' ') + 1).Trim(); + + // comments + if (role == "##") + { + continue; + } + + samples.Add(new RoleDialogModel(role, content)); + } + + return samples; + } + + public List GetFunctions(string functionsJson) + { + var functions = new List(); + if (!string.IsNullOrEmpty(functionsJson)) + { + functions = JsonSerializer.Deserialize>(functionsJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true + }); + } + + return functions; + } + + public async Task GetChatCompletionsAsync(Agent agent, + List conversations, + Func onMessageReceived, + Func onFunctionExecuting) + { + var client = GetClient(); + var chatCompletionsOptions = PrepareOptions(agent, conversations); + + var response = await client.GetChatCompletionsAsync(_settings.GPT4.DeploymentModel, chatCompletionsOptions); + var choice = response.Value.Choices[0]; + var message = choice.Message; + + if (choice.FinishReason == CompletionsFinishReason.FunctionCall) + { + _logger.LogInformation($"[{agent.Name}]: {message.FunctionCall.Name} => {message.FunctionCall.Arguments}"); + + var funcContextIn = new RoleDialogModel(AgentRole.Function, message.Content) + { + CurrentAgentId = agent.Id, + FunctionName = message.FunctionCall.Name, + FunctionArgs = message.FunctionCall.Arguments, + Channel = conversations.Last().Channel + }; + + // Execute functions + await onFunctionExecuting(funcContextIn); + } + else + { + _logger.LogInformation($"[{agent.Name}] {message.Role}: {message.Content}"); + + var msg = new RoleDialogModel(AgentRole.Assistant, message.Content) + { + CurrentAgentId= agent.Id, + Channel = conversations.Last().Channel + }; + + // Text response received + await onMessageReceived(msg); + } + + return true; + } + + public async Task GetChatCompletionsStreamingAsync(Agent agent, List conversations, Func onMessageReceived) + { + var client = new OpenAIClient(new Uri(_settings.Endpoint), new AzureKeyCredential(_settings.ApiKey)); + var chatCompletionsOptions = PrepareOptions(agent, conversations); + + var response = await client.GetChatCompletionsStreamingAsync(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions); + using StreamingChatCompletions streaming = response.Value; + + string output = ""; + await foreach (var choice in streaming.GetChoicesStreaming()) + { + if (choice.FinishReason == CompletionsFinishReason.FunctionCall) + { + var args = ""; + await foreach (var message in choice.GetMessageStreaming()) + { + if (message.FunctionCall == null || message.FunctionCall.Arguments == null) + continue; + Console.Write(message.FunctionCall.Arguments); + args += message.FunctionCall.Arguments; + + } + await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), args)); + continue; + } + + await foreach (var message in choice.GetMessageStreaming()) + { + if (message.Content == null) + continue; + Console.Write(message.Content); + output += message.Content; + + _logger.LogInformation(message.Content); + + await onMessageReceived(new RoleDialogModel(message.Role.ToString(), message.Content)); + } + + output = ""; + } + + return true; + } + + + private ChatCompletionsOptions PrepareOptions(Agent agent, List conversations) + { + var chatCompletionsOptions = new ChatCompletionsOptions(); + + if (!string.IsNullOrEmpty(agent.Instruction)) + { + chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.System, agent.Instruction)); + } + + if (!string.IsNullOrEmpty(agent.Knowledges)) + { + chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.System, agent.Knowledges)); + } + + var samples = GetChatSamples(agent.Samples); + foreach (var message in samples) + { + chatCompletionsOptions.Messages.Add(new ChatMessage(message.Role, message.Content)); + } + + var functions = GetFunctions(agent.Functions); + foreach (var function in functions) + { + chatCompletionsOptions.Functions.Add(new FunctionDefinition + { + Name = function.Name, + Description = function.Description, + Parameters = BinaryData.FromObjectAsJson(function.Parameters) + }); + } + + foreach (var message in conversations) + { + if (message.Role == ChatRole.Function) + { + chatCompletionsOptions.Messages.Add(new ChatMessage(message.Role, message.Content) + { + Name = message.FunctionName + }); + } + else + { + chatCompletionsOptions.Messages.Add(new ChatMessage(message.Role, message.Content)); + } + } + + // https://community.openai.com/t/cheat-sheet-mastering-temperature-and-top-p-in-chatgpt-api-a-few-tips-and-tricks-on-controlling-the-creativity-deterministic-output-of-prompt-responses/172683 + chatCompletionsOptions.Temperature = 0.5f; + chatCompletionsOptions.NucleusSamplingFactor = 0.5f; + + var convSetting = _services.GetRequiredService(); + if (convSetting.ShowVerboseLog) + { + var verbose = string.Join("\n", chatCompletionsOptions.Messages.Select(x => + { + return x.Role == ChatRole.Function ? + $"{x.Role}: {x.Name} {x.Content}" : + $"{x.Role}: {x.Content}"; + })); + _logger.LogInformation(verbose); + } + + return chatCompletionsOptions; + } +} diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Settings/AzureOpenAiSettings.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Settings/AzureOpenAiSettings.cs index 2a3cbf6e..510bec5a 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Settings/AzureOpenAiSettings.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Settings/AzureOpenAiSettings.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Routing.Settings; + namespace BotSharp.Plugin.AzureOpenAI.Settings; public class AzureOpenAiSettings @@ -6,4 +8,6 @@ public class AzureOpenAiSettings public string Endpoint { get; set; } = string.Empty; public DeploymentModelSetting DeploymentModel { get; set; } = new DeploymentModelSetting(); + + public GPT4Settings GPT4 { get; set; } }