diff --git a/BotSharp.sln b/BotSharp.sln index a1c1ddb91..93f289ba9 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -117,6 +117,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Graph", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.AudioHandler", "src\Plugins\BotSharp.Plugin.AudioHandler\BotSharp.Plugin.AudioHandler.csproj", "{F57F4862-F8D4-44A1-AC12-5C131B5C9785}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.SideCar", "src\Infrastructure\BotSharp.Core.SideCar\BotSharp.Core.SideCar.csproj", "{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -469,6 +471,14 @@ Global {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|Any CPU.Build.0 = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.ActiveCfg = Release|Any CPU {F57F4862-F8D4-44A1-AC12-5C131B5C9785}.Release|x64.Build.0 = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Debug|x64.Build.0 = Debug|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|Any CPU.Build.0 = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.ActiveCfg = Release|Any CPU + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -525,6 +535,7 @@ Global {97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C} {EBFE97DA-D0BA-48BA-8B5D-083B60348D1D} = {97A0B191-64D7-4F8A-BFE8-1BFCC5E247E1} {F57F4862-F8D4-44A1-AC12-5C131B5C9785} = {51AFE054-AE99-497D-A593-69BAEFB5106F} + {6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj index 474dcf569..5be126b8e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj +++ b/src/Infrastructure/BotSharp.Abstraction/BotSharp.Abstraction.csproj @@ -1,4 +1,4 @@ - + $(TargetFramework) @@ -38,6 +38,7 @@ + diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs index de26ef5bb..df1239222 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs @@ -19,4 +19,8 @@ IConversationStateService SetState(string name, T value, bool isNeedVersion = bool RemoveState(string name); void CleanStates(params string[] excludedStates); void Save(); + + ConversationState GetCurrentState(); + void SetCurrentState(ConversationState state); + void ResetCurrentState(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationContext.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationContext.cs new file mode 100644 index 000000000..0b0b08461 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/Models/ConversationContext.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Conversations.Models; + +public class ConversationContext +{ + public ConversationState State { get; set; } + public List Dialogs { get; set; } = new(); + public List Breakpoints { get; set; } = new(); + public int RecursiveCounter { get; set; } + public Stack RoutingStack { get; set; } = new(); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 4f604dc42..57900aa0b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Shared; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; using BotSharp.Abstraction.Users.Models; @@ -8,7 +9,7 @@ namespace BotSharp.Abstraction.Repositories; -public interface IBotSharpRepository +public interface IBotSharpRepository : IHaveServiceProvider { #region Plugin PluginConfig GetPluginConfig(); diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs index 3c93976bd..f75e8e0e6 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingContext.cs @@ -17,4 +17,15 @@ public interface IRoutingContext void PopTo(string agentId, string reason); void Replace(string agentId, string? reason = null); void Empty(string? reason = null); + + + int CurrentRecursionDepth { get; } + int GetRecursiveCounter(); + void IncreaseRecursiveCounter(); + void SetRecursiveCounter(int counter); + void ResetRecursiveCounter(); + + Stack GetAgentStack(); + void SetAgentStack(Stack stack); + void ResetAgentStack(); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs index 3b221f4d2..c0f7c81c3 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/IRoutingService.cs @@ -27,7 +27,11 @@ public interface IRoutingService RoutingRule[] GetRulesByAgentId(string id); List GetHandlers(Agent router); - void ResetRecursiveCounter(); + + //void ResetRecursiveCounter(); + //int GetRecursiveCounter(); + //void SetRecursiveCounter(int counter); + Task InvokeAgent(string agentId, List dialogs); Task InvokeFunction(string name, RoleDialogModel messages); Task InstructLoop(RoleDialogModel message, List dialogs); diff --git a/src/Infrastructure/BotSharp.Abstraction/Shared/IHaveServiceProvider.cs b/src/Infrastructure/BotSharp.Abstraction/Shared/IHaveServiceProvider.cs new file mode 100644 index 000000000..0a68cadd5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Shared/IHaveServiceProvider.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Shared; + +public interface IHaveServiceProvider +{ + IServiceProvider ServiceProvider { get; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAspect.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAspect.cs new file mode 100644 index 000000000..6580c56c8 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAspect.cs @@ -0,0 +1,168 @@ +using AspectInjector.Broker; +using BotSharp.Abstraction.Shared; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace BotSharp.Abstraction.SideCar.Attributes; + +[Aspect(Scope.PerInstance)] +public class SideCarAspect +{ + [Advice(Kind.Around)] + public object Handle( + [Argument(Source.Target)] Func target, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Instance)] object instance, + [Argument(Source.ReturnType)] Type retType, + [Argument(Source.Name)] string name, + [Argument(Source.Metadata)] MethodBase metaData, + [Argument(Source.Triggers)] Attribute[] triggers) + { + object value; + var serviceProvider = ((IHaveServiceProvider)instance).ServiceProvider; + + if (typeof(Task).IsAssignableFrom(retType)) + { + var syncResultType = retType.IsConstructedGenericType ? retType.GenericTypeArguments[0] : typeof(void); + value = CallAsyncMethod(serviceProvider, syncResultType, name, target, args); + } + else + { + value = CallSyncMethod(serviceProvider, retType, name, target, args); + } + + return value; + } + + + private static MethodInfo GetMethod(string name) + { + return typeof(SideCarAspect).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static); + } + + private object CallAsyncMethod(IServiceProvider serviceProvider, Type retType, string methodName, Func target, object[] args) + { + var sidecar = serviceProvider.GetService(); + var sidecarMethod = sidecar?.GetType()?.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance); + + object value; + var enabled = sidecar != null && sidecar.IsEnabled() && sidecarMethod != null; + + if (retType == typeof(void)) + { + if (enabled) + { + + value = GetMethod(nameof(CallAsync)).Invoke(this, [sidecar, sidecarMethod, args]); + } + else + { + value = GetMethod(nameof(WrapAsync)).Invoke(this, [target, args]); + } + } + else + { + if (enabled) + { + value = GetMethod(nameof(CallGenericAsync)).MakeGenericMethod(retType).Invoke(this, [sidecar, sidecarMethod, args]); + } + else + { + value = GetMethod(nameof(WrapGenericAsync)).MakeGenericMethod(retType).Invoke(this, [target, args]); + } + } + + return value; + } + + private object CallSyncMethod(IServiceProvider serviceProvider, Type retType, string methodName, Func target, object[] args) + { + var sidecar = serviceProvider.GetService(); + var sidecarMethod = sidecar?.GetType()?.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance); + + object value; + var enabled = sidecar != null && sidecarMethod != null && sidecar.IsEnabled(); + + if (retType == typeof(void)) + { + if (enabled) + { + value = GetMethod(nameof(CallSync)).Invoke(this, [sidecar, sidecarMethod, args]); + } + else + { + value = GetMethod(nameof(WrapSync)).Invoke(this, [target, args]); + } + } + else + { + if (enabled) + { + value = GetMethod(nameof(CallGenericSync)).MakeGenericMethod(retType).Invoke(this, [sidecar, sidecarMethod, args]); + } + else + { + value = GetMethod(nameof(WrapGenericSync)).MakeGenericMethod(retType).Invoke(this, [target, args]); + } + } + + return value; + } + + + #region Call Side car method + private static async Task CallGenericAsync(object instance, MethodInfo method, object[] args) + { + var res = await (Task)method.Invoke(instance, args); + return res; + } + + private static async Task CallAsync(object instance, MethodInfo method, object[] args) + { + await (Task)method.Invoke(instance, args); + return; + } + + private static T CallGenericSync(object instance, MethodInfo method, object[] args) + { + var res = (T)method.Invoke(instance, args); + return res; + } + + private static void CallSync(object instance, MethodInfo method, object[] args) + { + method.Invoke(instance, args); + return; + } + #endregion + + + #region Call original method + private static T WrapGenericSync(Func target, object[] args) + { + T res; + res = (T)target(args); + return res; + } + + private static async Task WrapGenericAsync(Func target, object[] args) + { + T res; + res = await (Task)target(args); + return res; + } + + + private static void WrapSync(Func target, object[] args) + { + target(args); + return; + } + + private static async Task WrapAsync(Func target, object[] args) + { + await (Task)target(args); + return; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs new file mode 100644 index 000000000..2c5c259f0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/Attributes/SideCarAttribute.cs @@ -0,0 +1,13 @@ +using AspectInjector.Broker; + +namespace BotSharp.Abstraction.SideCar.Attributes; + +[AttributeUsage(AttributeTargets.Method, Inherited = true)] +[Injection(typeof(SideCarAspect))] +public class SideCarAttribute : Attribute +{ + public SideCarAttribute() + { + + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs new file mode 100644 index 000000000..9a1316fbd --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/SideCar/IConversationSideCar.cs @@ -0,0 +1,13 @@ +namespace BotSharp.Abstraction.SideCar; + +public interface IConversationSideCar +{ + string Provider { get; } + + bool IsEnabled(); + void AppendConversationDialogs(string conversationId, List messages); + List GetConversationDialogs(string conversationId); + void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint); + ConversationBreakpoint? GetConversationBreakpoint(string conversationId); + Task SendMessage(string agentId, string text, PostbackMessageModel? postback = null, List? states = null); +} diff --git a/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj b/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj new file mode 100644 index 000000000..4b661c2a5 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.SideCar/BotSharp.Core.SideCar.csproj @@ -0,0 +1,16 @@ + + + + $(TargetFramework) + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(SolutionDir)packages + enable + + + + + + + diff --git a/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs b/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs new file mode 100644 index 000000000..efacd3082 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.SideCar/BotSharpSideCarPlugin.cs @@ -0,0 +1,28 @@ +using BotSharp.Abstraction.Plugins; +using BotSharp.Abstraction.Settings; +using BotSharp.Core.SideCar.Services; +using Microsoft.Extensions.Configuration; + +namespace BotSharp.Core.SideCar; + +public class BotSharpSideCarPlugin : IBotSharpPlugin +{ + public string Id => "06e5a276-bba0-45af-9625-889267c341c9"; + public string Name => "Side car"; + public string Description => "Provides side car for calling agent cluster in conversation"; + + public SettingsMeta Settings => new SettingsMeta("SideCar"); + public object GetNewSettingsInstance() => new SideCarSettings(); + + public void RegisterDI(IServiceCollection services, IConfiguration config) + { + var settings = new SideCarSettings(); + config.Bind("SideCar", settings); + services.AddSingleton(settings); + + if (settings.Conversation.Provider == "botsharp") + { + services.AddScoped(); + } + } +} diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs new file mode 100644 index 000000000..fda96b176 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.SideCar/Services/BotSharpConversationSideCar.cs @@ -0,0 +1,135 @@ +namespace BotSharp.Core.SideCar.Services; + +public class BotSharpConversationSideCar : IConversationSideCar +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + private Stack contextStack = new(); + + private bool enabled = false; + + public string Provider => "botsharp"; + + public BotSharpConversationSideCar( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public bool IsEnabled() + { + return enabled; + } + + public void AppendConversationDialogs(string conversationId, List messages) + { + if (contextStack.IsNullOrEmpty()) return; + + var top = contextStack.Peek(); + top.Dialogs.AddRange(messages); + } + + public List GetConversationDialogs(string conversationId) + { + if (contextStack.IsNullOrEmpty()) + { + return new List(); + } + + return contextStack.Peek().Dialogs; + } + + public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) + { + if (contextStack.IsNullOrEmpty()) return; + + var top = contextStack.Peek().Breakpoints; + top.Add(breakpoint); + } + + public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) + { + if (contextStack.IsNullOrEmpty()) + { + return null; + } + + var top = contextStack.Peek().Breakpoints; + return top.LastOrDefault(); + } + + public async Task SendMessage(string agentId, string text, + PostbackMessageModel? postback = null, List? states = null) + { + BeforeExecute(); + var response = await InnerExecute(agentId, text, postback, states); + AfterExecute(); + return response; + } + + private async Task InnerExecute(string agentId, string text, + PostbackMessageModel? postback = null, List? states = null) + { + var conv = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + + var inputMsg = new RoleDialogModel(AgentRole.User, text); + routing.Context.SetMessageId(conv.ConversationId, inputMsg.MessageId); + states?.ForEach(x => state.SetState(x.Key, x.Value, activeRounds: x.ActiveRounds, source: StateSource.External)); + + var response = new RoleDialogModel(AgentRole.Assistant, string.Empty); + await conv.SendMessage(agentId, inputMsg, + replyMessage: postback, + async msg => + { + response.Content = !string.IsNullOrEmpty(msg.SecondaryContent) ? msg.SecondaryContent : msg.Content; + response.FunctionName = msg.FunctionName; + response.RichContent = msg.SecondaryRichContent ?? msg.RichContent; + response.Instruction = msg.Instruction; + response.Data = msg.Data; + }); + + return response; + } + + private void BeforeExecute() + { + enabled = true; + var state = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); + + var node = new ConversationContext + { + State = state.GetCurrentState(), + Dialogs = new(), + Breakpoints = new(), + RecursiveCounter = routing.Context.GetRecursiveCounter(), + RoutingStack = routing.Context.GetAgentStack() + }; + contextStack.Push(node); + + // Reset + state.ResetCurrentState(); + routing.Context.ResetRecursiveCounter(); + routing.Context.ResetAgentStack(); + + } + + private void AfterExecute() + { + var state = _services.GetRequiredService(); + var routing = _services.GetRequiredService(); + + var node = contextStack.Pop(); + + // Recover + state.SetCurrentState(node.State); + routing.Context.SetRecursiveCounter(node.RecursiveCounter); + routing.Context.SetAgentStack(node.RoutingStack); + enabled = false; + } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Settings/SideCarSettings.cs b/src/Infrastructure/BotSharp.Core.SideCar/Settings/SideCarSettings.cs new file mode 100644 index 000000000..24b60b2a4 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.SideCar/Settings/SideCarSettings.cs @@ -0,0 +1,11 @@ +namespace BotSharp.Core.SideCar.Settings; + +public class SideCarSettings +{ + public BaseSetting Conversation { get; set; } +} + +public class BaseSetting +{ + public string Provider { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.SideCar/Using.cs b/src/Infrastructure/BotSharp.Core.SideCar/Using.cs new file mode 100644 index 000000000..d047ee152 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.SideCar/Using.cs @@ -0,0 +1,20 @@ +global using System; +global using System.Collections.Generic; +global using System.Text; +global using System.Threading.Tasks; +global using System.Linq; +global using System.Text.Json; +global using System.Net.Mime; +global using System.Net.Http; +global using System.Threading; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using BotSharp.Abstraction.Agents.Enums; +global using BotSharp.Abstraction.Conversations; +global using BotSharp.Abstraction.Conversations.Enums; +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Models; +global using BotSharp.Abstraction.Routing; +global using BotSharp.Abstraction.SideCar; +global using BotSharp.Abstraction.Utilities; +global using BotSharp.Core.SideCar.Settings; \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 59ce88c2c..375cf3e69 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs @@ -1,7 +1,6 @@ using BotSharp.Abstraction.Messaging; using BotSharp.Abstraction.Messaging.Models.RichContent; using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Core.Routing.Planning; namespace BotSharp.Core.Conversations.Services; @@ -90,7 +89,7 @@ public async Task SendMessage(string agentId, response = await routing.InstructDirect(agent, message); } - routing.ResetRecursiveCounter(); + routing.Context.ResetRecursiveCounter(); } await HandleAssistantMessage(response, onMessageReceived); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 070f4c597..6c856591b 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -98,6 +98,7 @@ public async Task NewConversation(Conversation sess) var record = sess; record.Id = sess.Id.IfNullOrEmptyAs(Guid.NewGuid().ToString()); record.UserId = sess.UserId.IfNullOrEmptyAs(foundUserId); + record.Tags = sess.Tags; record.Title = "New Conversation"; db.CreateNewConversation(record); @@ -141,6 +142,7 @@ public List GetDialogHistory(int lastCount = 100, bool fromBrea { var db = _services.GetRequiredService(); var breakpoint = db.GetConversationBreakpoint(_conversationId); + if (breakpoint != null) { dialogs = dialogs.Where(x => x.CreatedAt >= breakpoint.Breakpoint).ToList(); @@ -151,9 +153,7 @@ public List GetDialogHistory(int lastCount = 100, bool fromBrea } } - return dialogs - .TakeLast(lastCount) - .ToList(); + return dialogs.TakeLast(lastCount).ToList(); } public void SetConversationId(string conversationId, List states, bool isReadOnly = false) diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs index 7a3d1d5d0..8ee4c6626 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -384,4 +384,23 @@ private bool CheckArgType(string name, string value) } return true; } + + public ConversationState GetCurrentState() + { + var values = _curStates.Values.ToList(); + var copy = JsonSerializer.Deserialize>(JsonSerializer.Serialize(values)); + return new ConversationState(copy ?? new()); + } + + public void SetCurrentState(ConversationState state) + { + var values = _curStates.Values.ToList(); + var copy = JsonSerializer.Deserialize>(JsonSerializer.Serialize(values)); + _curStates = new ConversationState(copy ?? new()); + } + + public void ResetCurrentState() + { + _curStates.Clear(); + } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 80c37f7a5..d2c519ebb 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -8,6 +8,8 @@ namespace BotSharp.Core.Repository; public class BotSharpDbContext : Database, IBotSharpRepository { + public IServiceProvider ServiceProvider => throw new NotImplementedException(); + #region Plugin public PluginConfig GetPluginConfig() => throw new NotImplementedException(); public void SavePluginConfig(PluginConfig config) => throw new NotImplementedException(); @@ -90,12 +92,14 @@ public List GetLastConversations() public List GetIdleConversations(int batchSize, int messageLimit, int bufferHours, IEnumerable excludeAgentIds) => throw new NotImplementedException(); + [SideCar] public List GetConversationDialogs(string conversationId) => throw new NotImplementedException(); public ConversationState GetConversationStates(string conversationId) => throw new NotImplementedException(); + [SideCar] public void AppendConversationDialogs(string conversationId, List dialogs) => throw new NotImplementedException(); @@ -108,9 +112,11 @@ public bool UpdateConversationTags(string conversationId, List tags) public bool UpdateConversationMessage(string conversationId, UpdateMessageRequest request) => throw new NotImplementedException(); + [SideCar] public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) => throw new NotImplementedException(); + [SideCar] public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) => throw new NotImplementedException(); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index 8634eee31..59da45a5c 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -190,7 +190,7 @@ private void UpdateAgentInstructions(string agentId, string instruction, List conversationIds) return true; } + [SideCar] public List GetConversationDialogs(string conversationId) { var dialogs = new List(); @@ -78,6 +79,7 @@ public List GetConversationDialogs(string conversationId) return dialogs; } + [SideCar] public void AppendConversationDialogs(string conversationId, List dialogs) { var convDir = FindConversationDirectory(conversationId); @@ -182,6 +184,7 @@ public bool UpdateConversationMessage(string conversationId, UpdateMessageReques return true; } + [SideCar] public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { var convDir = FindConversationDirectory(conversationId); @@ -220,6 +223,7 @@ public void UpdateConversationBreakpoint(string conversationId, ConversationBrea } } + [SideCar] public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) { var convDir = FindConversationDirectory(conversationId); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs index 46f39aaa7..f3e1fddf7 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -171,6 +171,8 @@ private IQueryable UserAgents } } + public IServiceProvider ServiceProvider => _services; + #region Private methods private void DeleteBeforeCreateDirectory(string dir) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs b/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs index 93fa2f3b5..f6b05375e 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs @@ -147,7 +147,7 @@ public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, Ro context.Pop(); var routing = _services.GetRequiredService(); - routing.ResetRecursiveCounter(); + routing.Context.ResetRecursiveCounter(); return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs index a77397d09..5c1f19032 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingContext.cs @@ -1,5 +1,4 @@ using BotSharp.Abstraction.Routing.Settings; -using BotSharp.Abstraction.Utilities; namespace BotSharp.Core.Routing; @@ -10,6 +9,7 @@ public class RoutingContext : IRoutingContext private string[] _routerAgentIds; private string _conversationId; private string _messageId; + private int _currentRecursionDepth = 0; public RoutingContext(IServiceProvider services, RoutingSettings setting) { @@ -20,9 +20,9 @@ public RoutingContext(IServiceProvider services, RoutingSettings setting) public int AgentCount => _stack.Count; public string ConversationId => _conversationId; public string MessageId => _messageId; + public int CurrentRecursionDepth => _currentRecursionDepth; - private Stack _stack { get; set; } - = new Stack(); + private Stack _stack { get; set; } = new(); /// /// Intent name @@ -208,4 +208,39 @@ public void SetMessageId(string conversationId, string messageId) _conversationId = conversationId; _messageId = messageId; } + + public int GetRecursiveCounter() + { + return _currentRecursionDepth; + } + + public void IncreaseRecursiveCounter() + { + _currentRecursionDepth++; + } + + public void SetRecursiveCounter(int counter) + { + _currentRecursionDepth = counter; + } + + public void ResetRecursiveCounter() + { + _currentRecursionDepth = 0; + } + + public Stack GetAgentStack() + { + return new Stack(_stack); + } + + public void SetAgentStack(Stack stack) + { + _stack = new Stack(stack); + } + + public void ResetAgentStack() + { + _stack.Clear(); + } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index ccd708c35..25ab3552a 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -4,14 +4,15 @@ namespace BotSharp.Core.Routing; public partial class RoutingService { - private int _currentRecursionDepth = 0; + //private int _currentRecursionDepth = 0; public async Task InvokeAgent(string agentId, List dialogs) { var agentService = _services.GetRequiredService(); var agent = await agentService.LoadAgent(agentId); - _currentRecursionDepth++; - if (_currentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) + //_currentRecursionDepth++; + Context.IncreaseRecursiveCounter(); + if (Context.CurrentRecursionDepth > agent.LlmConfig.MaxRecursionDepth) { _logger.LogWarning($"Current recursive call depth greater than {agent.LlmConfig.MaxRecursionDepth}, which will cause unexpected result."); return false; @@ -36,8 +37,7 @@ public async Task InvokeAgent(string agentId, List dialog if (response.Role == AgentRole.Function) { - message = RoleDialogModel.From(message, - role: AgentRole.Function); + message = RoleDialogModel.From(message, role: AgentRole.Function); if (response.FunctionName != null && response.FunctionName.Contains("/")) { response.FunctionName = response.FunctionName.Split("/").Last(); @@ -57,9 +57,7 @@ public async Task InvokeAgent(string agentId, List dialog response.Content = "Apologies, I'm not quite sure I understand. Could you please provide additional clarification or context?"; } - message = RoleDialogModel.From(message, - role: AgentRole.Assistant, - content: response.Content); + message = RoleDialogModel.From(message, role: AgentRole.Assistant, content: response.Content); message.CurrentAgentId = agent.Id; dialogs.Add(message); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs index b7c489e16..770acaf9c 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.cs @@ -16,12 +16,23 @@ public partial class RoutingService : IRoutingService public IRoutingContext Context => _context; public Agent Router => _router; - public void ResetRecursiveCounter() - { - _currentRecursionDepth = 0; - } - - public RoutingService(IServiceProvider services, + //public int GetRecursiveCounter() + //{ + // return _currentRecursionDepth; + //} + + //public void SetRecursiveCounter(int counter) + //{ + // _currentRecursionDepth = counter; + //} + + //public void ResetRecursiveCounter() + //{ + // _currentRecursionDepth = 0; + //} + + public RoutingService( + IServiceProvider services, RoutingSettings settings, IRoutingContext context, ILogger logger) diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index e28eed725..8a0ca2af2 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -33,6 +33,7 @@ global using BotSharp.Abstraction.Translation.Attributes; global using BotSharp.Abstraction.Messaging.Enums; global using BotSharp.Abstraction.Knowledges.Models; +global using BotSharp.Abstraction.SideCar.Attributes; global using BotSharp.Core.Repository; global using BotSharp.Core.Routing; global using BotSharp.Core.Agents.Services; diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 29c6df82c..b282fa53d 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.SideCar; using Microsoft.AspNetCore.SignalR; namespace BotSharp.Plugin.ChatHub.Hooks; @@ -32,6 +33,8 @@ public ChatHubConversationHook( public override async Task OnConversationInitialized(Conversation conversation) { + if (!AllowSendingMessage()) return; + var userService = _services.GetRequiredService(); var conv = ConversationViewModel.FromSession(conversation); @@ -44,6 +47,8 @@ public override async Task OnConversationInitialized(Conversation conversation) public override async Task OnMessageReceived(RoleDialogModel message) { + if (!AllowSendingMessage()) return; + var conv = _services.GetRequiredService(); var userService = _services.GetRequiredService(); var sender = await userService.GetMyProfile(); @@ -90,6 +95,8 @@ public override async Task OnPostbackMessageReceived(RoleDialogModel message, Po public override async Task OnResponseGenerated(RoleDialogModel message) { + if (!AllowSendingMessage()) return; + var conv = _services.GetRequiredService(); var json = JsonSerializer.Serialize(new ChatResponseModel() { @@ -156,6 +163,12 @@ public override async Task OnMessageDeleted(string conversationId, string messag } #region Private methods + private bool AllowSendingMessage() + { + var sidecar = _services.GetService(); + return sidecar == null || !sidecar.IsEnabled(); + } + private async Task InitClientConversation(ConversationViewModel conversation) { await _chatHub.Clients.User(_user.Id).SendAsync(INIT_CLIENT_CONVERSATION, conversation); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index d8159da14..85fa10337 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs @@ -70,6 +70,7 @@ public bool DeleteConversations(IEnumerable conversationIds) || contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0; } + [SideCar] public List GetConversationDialogs(string conversationId) { var dialogs = new List(); @@ -83,6 +84,7 @@ public List GetConversationDialogs(string conversationId) return formattedDialog ?? new List(); } + [SideCar] public void AppendConversationDialogs(string conversationId, List dialogs) { if (string.IsNullOrEmpty(conversationId)) return; @@ -159,6 +161,7 @@ public bool UpdateConversationMessage(string conversationId, UpdateMessageReques return true; } + [SideCar] public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { if (string.IsNullOrEmpty(conversationId)) return; @@ -176,6 +179,7 @@ public void UpdateConversationBreakpoint(string conversationId, ConversationBrea _dc.ConversationStates.UpdateOne(filterState, updateState); } + [SideCar] public ConversationBreakpoint? GetConversationBreakpoint(string conversationId) { if (string.IsNullOrEmpty(conversationId)) diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs index 689c06be3..258c18838 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -22,4 +22,6 @@ public MongoRepository( IsUpsert = true, }; } + + public IServiceProvider ServiceProvider => _services; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs index 7c74b648f..13b2739bb 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Using.cs @@ -8,6 +8,7 @@ global using BotSharp.Abstraction.Utilities; global using BotSharp.Abstraction.Plugins; global using BotSharp.Abstraction.Translation.Models; +global using BotSharp.Abstraction.SideCar.Attributes; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using MongoDB.Bson; diff --git a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs index 2ecd9ac0e..565115a5e 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -92,7 +92,7 @@ public async Task AgentExecuted(Agent router, FunctionCallFromLlm inst, Ro } var routing = _services.GetRequiredService(); - routing.ResetRecursiveCounter(); + routing.Context.ResetRecursiveCounter(); return true; } diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 4bd62176e..a8f819d6e 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -29,6 +29,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 7f43ba284..8c4cd9aaa 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -151,6 +151,12 @@ } }, + "SideCar": { + "Conversation": { + "Provider": "botsharp" + } + }, + "WebBrowsing": { "Driver": "Playwright" }, @@ -321,6 +327,7 @@ "PluginLoader": { "Assemblies": [ "BotSharp.Core", + "BotSharp.Core.SideCar", "BotSharp.Logger", "BotSharp.Plugin.MongoStorage", "BotSharp.Plugin.Dashboard",