diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs index 0a9e2668..f1b71eb8 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentHookBase.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Conversations.Models; using Fluid; namespace BotSharp.Core.Agents.Services; @@ -67,9 +66,4 @@ public abstract class AgentHookBase : IAgentHook public virtual void OnAgentLoaded(Agent agent) { } - - public virtual bool OnAgentRouting(RoleDialogModel message, ref string id) - { - return true; - } } diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs new file mode 100644 index 00000000..b02f3d03 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.CallFunctions.cs @@ -0,0 +1,42 @@ +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Functions; + +namespace BotSharp.Core.Conversations.Services; + +public partial class ConversationService +{ + private async Task CallFunctions(RoleDialogModel msg) + { + var hooks = _services.GetServices().ToList(); + + // Invoke functions + var functions = _services.GetServices() + .Where(x => x.Name == msg.FunctionName) + .ToList(); + + if (functions.Count == 0) + { + msg.Content = $"Can't find function implementation of {msg.FunctionName}."; + _logger.LogError(msg.Content); + return; + } + + foreach (var fn in functions) + { + // Before executing functions + foreach (var hook in hooks) + { + await hook.OnFunctionExecuting(msg); + } + + // Execute function + await fn.Execute(msg); + + // After functions have been executed + foreach (var hook in hooks) + { + await hook.OnFunctionExecuted(msg); + } + } + } +} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs new file mode 100644 index 00000000..1180c7e6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.GetChatCompletionsAsyncRecursively.cs @@ -0,0 +1,101 @@ +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.MLTasks; + +namespace BotSharp.Core.Conversations.Services; + +public partial class ConversationService +{ + const int maxRecursiveDepth = 3; + int currentRecursiveDepth = 0; + + private async Task GetChatCompletionsAsyncRecursively(IChatCompletion chatCompletion, + string conversationId, + Agent agent, + List wholeDialogs, + Func onMessageReceived, + Func onFunctionExecuting) + { + currentRecursiveDepth++; + if (currentRecursiveDepth > maxRecursiveDepth) + { + _logger.LogError($"Exceed max current recursive depth."); + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, "System has exception, please try later.") + { + CurrentAgentId = agent.Id + }, onMessageReceived); + return false; + } + + var result = await chatCompletion.GetChatCompletionsAsync(agent, wholeDialogs, async msg => + { + await HandleAssistantMessage(msg, onMessageReceived); + + // Add to dialog history + _storage.Append(conversationId, agent.Id, msg); + }, async fn => + { + var preAgentId = agent.Id; + + await HandleFunctionMessage(fn, onFunctionExecuting); + + // Function executed has exception + if (fn.ExecutionResult == null) + { + await HandleAssistantMessage(new RoleDialogModel(AgentRole.Assistant, fn.Content) + { + CurrentAgentId = fn.CurrentAgentId + }, onMessageReceived); + return; + } + + fn.Content = fn.ExecutionResult; + + // Agent has been transferred + if (fn.CurrentAgentId != preAgentId) + { + var agentSettings = _services.GetRequiredService(); + 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); + } + + // Add to dialog history + _storage.Append(conversationId, preAgentId, 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 HandleAssistantMessage(RoleDialogModel msg, Func onMessageReceived) + { + var hooks = _services.GetServices().ToList(); + + // After chat completion hook + foreach (var hook in hooks) + { + await hook.AfterCompletion(msg); + } + + await onMessageReceived(msg); + } + + private async Task HandleFunctionMessage(RoleDialogModel msg, Func onFunctionExecuting) + { + // Save states + SaveStateByArgs(msg.FunctionArgs); + + // Call functions + await onFunctionExecuting(msg); + await CallFunctions(msg); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs new file mode 100644 index 00000000..b4ab98f5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -0,0 +1,93 @@ +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.MLTasks; + +namespace BotSharp.Core.Conversations.Services; + +public partial class ConversationService +{ + public async Task SendMessage(string agentId, string conversationId, + RoleDialogModel lastDialog, + Func onMessageReceived, + Func onFunctionExecuting) + { + var converation = await GetConversation(conversationId); + + // Create conversation if this conversation not exists + if (converation == null) + { + var sess = new Conversation + { + Id = conversationId, + AgentId = agentId + }; + converation = await NewConversation(sess); + } + + // conversation state + var stateService = _services.GetRequiredService(); + stateService.SetConversation(conversationId); + stateService.Load(); + + var router = _services.GetRequiredService(); + var agent = await router.LoadCurrentAgent(); + + _logger.LogInformation($"[{agent.Name}] {lastDialog.Role}: {lastDialog.Content}"); + + lastDialog.CurrentAgentId = agent.Id; + _storage.Append(conversationId, agent.Id, lastDialog); + + var wholeDialogs = GetDialogHistory(conversationId); + + // Get relevant domain knowledge + /*if (_settings.EnableKnowledgeBase) + { + var knowledge = _services.GetRequiredService(); + agent.Knowledges = await knowledge.GetKnowledges(new KnowledgeRetrievalModel + { + AgentId = agentId, + Question = string.Join("\n", wholeDialogs.Select(x => x.Content)) + }); + }*/ + + var hooks = _services.GetServices().ToList(); + + // Before chat completion hook + foreach (var hook in hooks) + { + hook.SetAgent(agent) + .SetConversation(converation); + + await hook.OnDialogsLoaded(wholeDialogs); + await hook.BeforeCompletion(); + } + + var chatCompletion = GetChatCompletion(); + var result = await GetChatCompletionsAsyncRecursively(chatCompletion, + conversationId, + agent, + wholeDialogs, + onMessageReceived, + onFunctionExecuting); + + return result; + } + + private void SaveStateByArgs(string args) + { + var stateService = _services.GetRequiredService(); + var jo = JsonSerializer.Deserialize(args); + if (jo is JsonElement root) + { + foreach (JsonProperty property in root.EnumerateObject()) + { + stateService.SetState(property.Name, property.Value.ToString()); + } + } + } + + public IChatCompletion GetChatCompletion() + { + var completions = _services.GetServices(); + return completions.FirstOrDefault(x => x.GetType().FullName.EndsWith(_settings.ChatCompletion)); + } +} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index b5e351e6..299ad8a6 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -1,11 +1,8 @@ -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; -using BotSharp.Abstraction.Functions; -using BotSharp.Abstraction.MLTasks; namespace BotSharp.Core.Conversations.Services; -public class ConversationService : IConversationService +public partial class ConversationService : IConversationService { private readonly ILogger _logger; private readonly IServiceProvider _services; @@ -70,194 +67,6 @@ public class ConversationService : IConversationService return record.ToConversation(); } - public async Task SendMessage(string agentId, string conversationId, - RoleDialogModel lastDialog, - Func onMessageReceived, - Func onFunctionExecuting) - { - var converation = await GetConversation(conversationId); - - // Create conversation if this conversation not exists - if (converation == null) - { - var sess = new Conversation - { - Id = conversationId, - AgentId = agentId - }; - converation = await NewConversation(sess); - } - - // conversation state - var stateService = _services.GetRequiredService(); - stateService.SetConversation(conversationId); - stateService.Load(); - - var router = _services.GetRequiredService(); - var agent = await router.LoadCurrentAgent(); - - _logger.LogInformation($"[{agent.Name}] {lastDialog.Role}: {lastDialog.Content}"); - - lastDialog.CurrentAgentId = agent.Id; - _storage.Append(conversationId, agent.Id, lastDialog); - - var wholeDialogs = GetDialogHistory(conversationId); - - // Get relevant domain knowledge - /*if (_settings.EnableKnowledgeBase) - { - var knowledge = _services.GetRequiredService(); - agent.Knowledges = await knowledge.GetKnowledges(new KnowledgeRetrievalModel - { - AgentId = agentId, - Question = string.Join("\n", wholeDialogs.Select(x => x.Content)) - }); - }*/ - - var hooks = _services.GetServices().ToList(); - - // Before chat completion hook - foreach (var hook in hooks) - { - hook.SetAgent(agent) - .SetConversation(converation); - - await hook.OnDialogsLoaded(wholeDialogs); - await hook.BeforeCompletion(); - } - - 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 HandleAssistantMessage(msg, onMessageReceived); - - // Add to dialog history - _storage.Append(conversationId, agent.Id, msg); - }, async fn => - { - var preAgentId = agent.Id; - - await HandleFunctionMessage(fn, onFunctionExecuting); - - fn.Content = fn.ExecutionResult; - - // Agent has been transferred - if (fn.CurrentAgentId != preAgentId) - { - var agentSettings = _services.GetRequiredService(); - 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); - } - - // Add to dialog history - _storage.Append(conversationId, preAgentId, 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 HandleAssistantMessage(RoleDialogModel msg, Func onMessageReceived) - { - var hooks = _services.GetServices().ToList(); - - // After chat completion hook - foreach (var hook in hooks) - { - await hook.AfterCompletion(msg); - } - - await onMessageReceived(msg); - } - - private async Task HandleFunctionMessage(RoleDialogModel msg, Func onFunctionExecuting) - { - // Save states - SaveStateByArgs(msg.FunctionArgs); - - // Call functions - await onFunctionExecuting(msg); - await CallFunctions(msg); - } - - private void SaveStateByArgs(string args) - { - var stateService = _services.GetRequiredService(); - var jo = JsonSerializer.Deserialize(args); - if (jo is JsonElement root) - { - foreach (JsonProperty property in root.EnumerateObject()) - { - stateService.SetState(property.Name, property.Value.ToString()); - } - } - } - - private async Task CallFunctions(RoleDialogModel msg) - { - var hooks = _services.GetServices().ToList(); - - // Invoke functions - var functions = _services.GetServices() - .Where(x => x.Name == msg.FunctionName) - .ToList(); - - if (functions.Count == 0) - { - _logger.LogError($"Can't find function implementation of {msg.FunctionName}."); - return; - } - - foreach (var fn in functions) - { - // Before executing functions - foreach (var hook in hooks) - { - await hook.OnFunctionExecuting(msg); - } - - // Execute function - await fn.Execute(msg); - - // After functions have been executed - foreach (var hook in hooks) - { - await hook.OnFunctionExecuted(msg); - } - } - } - - public IChatCompletion GetChatCompletion() - { - var completions = _services.GetServices(); - return completions.FirstOrDefault(x => x.GetType().FullName.EndsWith(_settings.ChatCompletion)); - } - public Task CleanHistory(string agentId) { throw new NotImplementedException();