diff --git a/docs/index.rst b/docs/index.rst index ca220b75..c3730fe6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -80,6 +80,7 @@ The main documentation for the site is organized into the following sections: :caption: Prompt Engineering prompt/intro + prompt/template .. _architecture-docs: diff --git a/docs/prompt/intro.md b/docs/prompt/intro.md index 2d10f036..da1ae5f2 100644 --- a/docs/prompt/intro.md +++ b/docs/prompt/intro.md @@ -1 +1,3 @@ -# Prompt Engineering \ No newline at end of file +# Prompt Engineering + +LLM uses prompt as input, and the model produces different outputs according to the input. \ No newline at end of file diff --git a/docs/prompt/template.md b/docs/prompt/template.md new file mode 100644 index 00000000..35260c44 --- /dev/null +++ b/docs/prompt/template.md @@ -0,0 +1,3 @@ +# Template + +We can define the prompt as a template, and the template can be changed according to variables, so that a instruction file can be used to generate a dynamic prompt. \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs index 65e523ab..8c01cbbc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStorage.cs @@ -5,6 +5,6 @@ namespace BotSharp.Abstraction.Conversations; public interface IConversationStorage { void InitStorage(string conversationId); - void Append(string conversationId, RoleDialogModel dialog); + void Append(string conversationId, Agent agent, RoleDialogModel dialog); List GetDialogs(string conversationId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs index 3886445a..3335617e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/RoleDialogModel.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Agents.Enums; + namespace BotSharp.Abstraction.Conversations.Models; public class RoleDialogModel @@ -40,6 +42,13 @@ public class RoleDialogModel public override string ToString() { - return $"{Role}: {Content}"; + if (Role == AgentRole.Function) + { + return $"{Role}: {FunctionName}"; + } + else + { + return $"{Role}: {Content}"; + } } } diff --git a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs index 26fecf8a..be3a170b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MLTasks/IChatCompletion.cs @@ -4,7 +4,10 @@ namespace BotSharp.Abstraction.MLTasks; public interface IChatCompletion { - // string GetChatCompletions(Agent agent, List conversations, Func onMessageReceived); - Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived); + Task GetChatCompletionsAsync(Agent agent, + List conversations, + Func onMessageReceived, + Func onFunctionExecuting); + Task GetChatCompletionsStreamingAsync(Agent agent, List conversations, Func onMessageReceived); } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs index 8f112392..1c28bad3 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentRouter.cs @@ -20,8 +20,8 @@ public class AgentRouter : IAgentRouting public async Task LoadCurrentAgent() { // Load current agent from state - var stateService = _services.GetRequiredService(); - var currentAgentId = stateService.GetState("agentId"); + var state = _services.GetRequiredService(); + var currentAgentId = state.GetState("agentId"); if (string.IsNullOrEmpty(currentAgentId)) { currentAgentId = _settings.RouterId; @@ -30,7 +30,7 @@ public class AgentRouter : IAgentRouting var agent = await agentService.LoadAgent(currentAgentId); // Set agent and trigger state changed - stateService.SetState("agentId", currentAgentId); + state.SetState("agentId", currentAgentId); return agent; } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/ConversationController.cs b/src/Infrastructure/BotSharp.Core/Conversations/ConversationController.cs index e9dec02b..8a26a569 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/ConversationController.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/ConversationController.cs @@ -47,14 +47,16 @@ public class ConversationController : ControllerBase, IApiAdapter var conv = _services.GetRequiredService(); var response = new MessageResponseModel(); + var stackMsg = new List(); await conv.SendMessage(agentId, conversationId, new RoleDialogModel("user", input.Text), async msg => - response.Text = msg.Content, + stackMsg.Add(msg), async fn => await Task.CompletedTask); + response.Text = string.Join("\r\n", stackMsg.Select(x => x.Content)); return response; } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 12b6fed7..78516b22 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -73,15 +73,10 @@ public class ConversationService : IConversationService } public async Task SendMessage(string agentId, string conversationId, - RoleDialogModel lastDalog, + RoleDialogModel lastDialog, Func onMessageReceived, Func onFunctionExecuting) { - lastDalog.CurrentAgentId = agentId; - _storage.Append(conversationId, lastDalog); - - var wholeDialogs = GetDialogHistory(conversationId); - var converation = await GetConversation(conversationId); // Create conversation if this conversation not exists @@ -103,6 +98,11 @@ public class ConversationService : IConversationService var router = _services.GetRequiredService(); var agent = await router.LoadCurrentAgent(); + lastDialog.CurrentAgentId = agent.Id; + _storage.Append(conversationId, agent, lastDialog); + + var wholeDialogs = GetDialogHistory(conversationId); + // Get relevant domain knowledge /*if (_settings.EnableKnowledgeBase) { @@ -127,100 +127,81 @@ public class ConversationService : IConversationService } var chatCompletion = GetChatCompletion(); + var result = await GetChatCompletionsAsyncRecursively(chatCompletion, + conversationId, + agent, + wholeDialogs, + onMessageReceived, + onFunctionExecuting); + + return result; + } + + private async Task GetChatCompletionsAsyncRecursively(IChatCompletion chatCompletion, + string conversationId, + Agent agent, + List wholeDialogs, + Func onMessageReceived, + Func onFunctionExecuting) + { var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg => { - await HandleMessage(conversationId, agent, msg, onMessageReceived, onFunctionExecuting); + await HandleAssistantMessage(msg, onMessageReceived); - if (msg.NeedReloadAgent) + // Add to dialog history + _storage.Append(conversationId, agent, msg); + }, async fn => + { + var preAgentId = agent.Id; + + await HandleFunctionMessage(fn, onFunctionExecuting); + + // Agent has been transferred + if (fn.CurrentAgentId != preAgentId) { - await HandleMessageIfAgentReloaded(conversationId, agent, msg, wholeDialogs, onMessageReceived, onFunctionExecuting); + var agentService = _services.GetRequiredService(); + agent = await agentService.LoadAgent(fn.CurrentAgentId); + + // Set state to make next conversation will go to this agent directly + var state = _services.GetRequiredService(); + state.SetState("agentId", fn.CurrentAgentId); } + + fn.Content = fn.ExecutionResult; + + // Add to dialog history + _storage.Append(conversationId, agent, fn); + + // After function is executed, pass the result to LLM to get a natural response + wholeDialogs.Add(fn); + + await GetChatCompletionsAsyncRecursively(chatCompletion, conversationId, agent, wholeDialogs, onMessageReceived, onFunctionExecuting); }); return result; } - private async Task HandleMessage(string conversationId, Agent agent, RoleDialogModel msg, - Func onMessageReceived, - Func onFunctionExecuting) + private async Task HandleAssistantMessage(RoleDialogModel msg, Func onMessageReceived) { - if (msg.Role == "function") + var hooks = _services.GetServices().ToList(); + + // After chat completion hook + foreach (var hook in hooks) { - // Save states - SaveStateByArgs(msg.FunctionArgs); - - // Call functions - await onFunctionExecuting(msg); - await CallFunctions(msg); - - // Add to dialog history - if (msg.ExecutionResult != null) - { - if (msg.NeedReloadAgent) - { - _logger.LogInformation($"Skipped append dialog log: {msg.FunctionName}\n{msg.FunctionArgs}\n{msg.ExecutionResult}"); - return; - } - - _storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content) - { - CurrentAgentId = agent.Id, - FunctionName = msg.FunctionName, - FunctionArgs = msg.FunctionArgs, - ExecutionResult = msg.ExecutionResult - }); - } + await hook.AfterCompletion(msg); } - else - { - // Add to dialog history - _storage.Append(conversationId, new RoleDialogModel(msg.Role, msg.Content) - { - CurrentAgentId = agent.Id - }); - var hooks = _services.GetServices().ToList(); - // After chat completion hook - foreach (var hook in hooks) - { - await hook.AfterCompletion(msg); - } - - await onMessageReceived(msg); - } + await onMessageReceived(msg); } - private async Task HandleMessageIfAgentReloaded(string conversationId, Agent agent, - RoleDialogModel msg, - List wholeDialogs, - Func onMessageReceived, - Func onFunctionExecuting) + private async Task HandleFunctionMessage(RoleDialogModel msg, Func onFunctionExecuting) { - var state = _services.GetRequiredService(); - var currentAgentId = state.GetState("agentId"); + // Save states + SaveStateByArgs(msg.FunctionArgs); - // Send to LLM to get final response when agent is switched. - var conv = _services.GetRequiredService(); - var chatCompletion = conv.GetChatCompletion(); - var agentService = _services.GetRequiredService(); - var newAgent = await agentService.LoadAgent(currentAgentId); - await chatCompletion.GetChatCompletionsAsync(newAgent, wholeDialogs, async newMsg => - { - if (newMsg.Role == AgentRole.Function) - { - await HandleMessage(conversationId, agent, newMsg, onMessageReceived, onFunctionExecuting); - } - else - { - msg.StopPropagate = true; - await onMessageReceived(newMsg); - - _storage.Append(conversationId, new RoleDialogModel(newMsg.Role, newMsg.Content) - { - CurrentAgentId = agent.Id - }); - } - }); + // Call functions + await onFunctionExecuting(msg); + await CallFunctions(msg); } private void SaveStateByArgs(string args) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs index 0d2d249e..38097106 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStorage.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; using System.IO; @@ -12,7 +13,7 @@ public class ConversationStorage : IConversationStorage _dbSettings = dbSettings; } - public void Append(string conversationId, RoleDialogModel dialog) + public void Append(string conversationId, Agent agent, RoleDialogModel dialog) { var conversationFile = GetStorageFile(conversationId); var sb = new StringBuilder(); @@ -21,7 +22,7 @@ public class ConversationStorage : IConversationStorage { var args = dialog.FunctionArgs.Replace("\r", " ").Replace("\n", " ").Trim(); - sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{dialog.CurrentAgentId}|{dialog.FunctionName}|{args}"); + sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agent.Name}|{dialog.FunctionName}|{args}"); var content = dialog.ExecutionResult.Replace("\r", " ").Replace("\n", " ").Trim(); if (string.IsNullOrEmpty(content)) @@ -32,7 +33,7 @@ public class ConversationStorage : IConversationStorage } else if (dialog.Role == AgentRole.Assistant) { - sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|||"); + sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agent.Name}||"); var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim(); if (string.IsNullOrEmpty(content)) { @@ -42,7 +43,7 @@ public class ConversationStorage : IConversationStorage } else { - sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{dialog.CurrentAgentId}||"); + sb.AppendLine($"{dialog.CreatedAt}|{dialog.Role}|{agent.Name}||"); var content = dialog.Content.Replace("\r", " ").Replace("\n", " ").Trim(); if (string.IsNullOrEmpty(content)) { diff --git a/src/Infrastructure/BotSharp.Core/Plugins/LLamaSharp/ChatCompletionProvider.cs b/src/Infrastructure/BotSharp.Core/Plugins/LLamaSharp/ChatCompletionProvider.cs index 20d67820..75e845ac 100644 --- a/src/Infrastructure/BotSharp.Core/Plugins/LLamaSharp/ChatCompletionProvider.cs +++ b/src/Infrastructure/BotSharp.Core/Plugins/LLamaSharp/ChatCompletionProvider.cs @@ -19,7 +19,10 @@ public class ChatCompletionProvider : IChatCompletion throw new NotImplementedException(); } - public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived) + public async Task GetChatCompletionsAsync(Agent agent, + List conversations, + Func onMessageReceived, + Func onFunctionExecuting) { var content = string.Join("\n", conversations.Select(x => $"{x.Role}: {x.Content.Replace("user:", "User:")}")).Trim(); content += "\nBob: "; diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs index f71475e3..111a5412 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ using Azure; using Azure.AI.OpenAI; +using BotSharp.Abstraction.Agents.Enums; using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Functions.Models; @@ -31,36 +32,6 @@ public class ChatCompletionProvider : IChatCompletion return client; } - /*public string GetChatCompletions(Agent agent, List conversations, Func onMessageReceived) - { - var client = GetClient(); - var chatCompletionsOptions = PrepareOptions(agent, conversations); - - var response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions); - var choice = response.Value.Choices[0]; - var message = choice.Message; - - if (choice.FinishReason == CompletionsFinishReason.FunctionCall) - { - response = HandleFunctionCall(message, - onMessageReceived, - chatCompletionsOptions).Result; - } - - choice = response.Value.Choices[0]; - message = choice.Message; - - _logger.LogInformation(message.Content); - - if (!string.IsNullOrEmpty(message.Content)) - { - onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content)) - .Wait(); - } - - return message.Content.Trim(); - }*/ - public List GetChatSamples(string sampleText) { var samples = new List(); @@ -107,7 +78,10 @@ public class ChatCompletionProvider : IChatCompletion return functions; } - public async Task GetChatCompletionsAsync(Agent agent, List conversations, Func onMessageReceived) + public async Task GetChatCompletionsAsync(Agent agent, + List conversations, + Func onMessageReceived, + Func onFunctionExecuting) { var client = GetClient(); var chatCompletionsOptions = PrepareOptions(agent, conversations); @@ -118,24 +92,29 @@ public class ChatCompletionProvider : IChatCompletion if (choice.FinishReason == CompletionsFinishReason.FunctionCall) { - response = await HandleFunctionCall(agent, - message, - onMessageReceived, - chatCompletionsOptions); - } + _logger.LogInformation($"[{agent.Name}]: {message.FunctionCall.Name} => {message.FunctionCall.Arguments}"); - if (response != null) - { - choice = response.Value.Choices[0]; - message = choice.Message; - - _logger.LogInformation(message.Content); - - if (!string.IsNullOrEmpty(message.Content)) + var funcContextIn = new RoleDialogModel(AgentRole.Function, message.Content) { - var msgByLlm = new RoleDialogModel(ChatRole.Assistant.ToString(), message.Content); - await onMessageReceived(msgByLlm); - } + CurrentAgentId = agent.Id, + FunctionName = message.FunctionCall.Name, + FunctionArgs = message.FunctionCall.Arguments + }; + + // 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 + }; + + // Text response received + await onMessageReceived(msg); } return true; @@ -185,56 +164,6 @@ public class ChatCompletionProvider : IChatCompletion return true; } - private async Task> HandleFunctionCall(Agent agent, - ChatMessage message, - Func onMessageReceived, - ChatCompletionsOptions chatCompletionsOptions) - { - Response response = default; - - if (message.FunctionCall == null || message.FunctionCall.Arguments == null) - { - return response; - } - - _logger.LogInformation($"{message.FunctionCall.Name}: {message.FunctionCall.Arguments}"); - var funcContextIn = new RoleDialogModel(ChatRole.Function.ToString(), message.Content) - { - CurrentAgentId = agent.Id, - FunctionName = message.FunctionCall.Name, - FunctionArgs = message.FunctionCall.Arguments - }; - - // Execute functions - await onMessageReceived(funcContextIn); - - if (funcContextIn.StopPropagate) - { - return response; - } - - if (funcContextIn.IsConversationEnd) - { - await onMessageReceived(new RoleDialogModel(ChatRole.Assistant.ToString(), funcContextIn.Content) - { - IsConversationEnd = true - }); - return response; - } - - // After function is executed, pass the result to LLM - if (funcContextIn.ExecutionResult != null) - { - chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.Function, funcContextIn.ExecutionResult) - { - Name = funcContextIn.FunctionName - }); - var client = GetClient(); - response = client.GetChatCompletions(_settings.DeploymentModel.ChatCompletionModel, chatCompletionsOptions); - } - - return response; - } private ChatCompletionsOptions PrepareOptions(Agent agent, List conversations) {