diff --git a/BotSharp.sln b/BotSharp.sln index a1c1ddb9..93f289ba 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 474dcf56..5be126b8 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/Browsing/Models/PageActionArgs.cs b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs index 698939e5..16232e6b 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Browsing/Models/PageActionArgs.cs @@ -30,6 +30,8 @@ public class PageActionArgs /// public string[]? IncludeResponseUrls { get; set; } + public List? Selectors { get; set; } + /// /// If set to true, the response will be stored in memory /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs index de26ef5b..df123922 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationStateService.cs @@ -19,4 +19,8 @@ public interface IConversationStateService 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 00000000..0b0b0846 --- /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/Files/Models/FileInformation.cs b/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileInformation.cs index fe767e16..f8dd9449 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileInformation.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/Models/FileInformation.cs @@ -3,7 +3,7 @@ namespace BotSharp.Abstraction.Files.Models; public class FileInformation { /// - /// External file url + /// External file url for display /// [JsonPropertyName("file_url")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -35,4 +35,11 @@ public class FileInformation [JsonPropertyName("file_extension")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? FileExtension { get; set; } = string.Empty; + + /// + /// External file url for download + /// + [JsonPropertyName("file_download_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? FileDownloadUrl { get; set; } = string.Empty; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginFilter.cs index bc5c4bf2..e7798d6c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginFilter.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginFilter.cs @@ -3,5 +3,6 @@ namespace BotSharp.Abstraction.Plugins.Models public class PluginFilter { public Pagination Pager { get; set; } = new Pagination(); + public IEnumerable? Names { get; set; } } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 4f604dc4..57900aa0 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 @@ using BotSharp.Abstraction.VectorStorage.Models; 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 3c93976b..f75e8e0e 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 3b221f4d..c0f7c81c 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 00000000..0a68cadd --- /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 00000000..6580c56c --- /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 00000000..2c5c259f --- /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 00000000..9a1316fb --- /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.Abstraction/Users/IAuthenticationHook.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs index 36b07f11..d953271f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IAuthenticationHook.cs @@ -25,9 +25,10 @@ public interface IAuthenticationHook /// /// User authenticated successfully /// + /// /// /// - bool UserAuthenticated(JwtSecurityToken token) + bool UserAuthenticated(User user, Token token) => true; /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs index f42b7f25..1e54c44e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserIdentity.cs @@ -13,8 +13,6 @@ public interface IUserIdentity /// string UserLanguage { get; } string? Phone { get; } - string? AffiliateId { get; } - string? EmployeeId { get; } string Type { get; } string Role { get; } string? RegionCode { get; } 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 00000000..4b661c2a --- /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 00000000..efacd308 --- /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 00000000..fda96b17 --- /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 00000000..24b60b2a --- /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 00000000..d047ee15 --- /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/Agents/Services/AgentService.CreateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs index 3c9e6bf2..df10ac4b 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Tasks.Models; +using BotSharp.Abstraction.Users.Enums; using System.IO; using System.Text.RegularExpressions; @@ -25,6 +26,20 @@ public partial class AgentService var user = _db.GetUserById(_user.Id); _db.BulkInsertAgents(new List { agentRecord }); + if (!UserConstant.AdminRoles.Contains(user.Role)) + { + _db.BulkInsertUserAgents(new List + { + new UserAgent + { + UserId = user.Id, + AgentId = agentRecord.Id, + Actions = new List { UserAction.Edit, UserAction.Chat }, + CreatedTime = DateTime.UtcNow, + UpdatedTime = DateTime.UtcNow + } + }); + } Utilities.ClearCache(); return await Task.FromResult(agentRecord); diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.SendMessage.cs index 59ce88c2..375cf3e6 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 partial class ConversationService 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 070f4c59..6c856591 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -98,6 +98,7 @@ public partial class ConversationService : IConversationService 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 partial class ConversationService : IConversationService { 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 partial class ConversationService : IConversationService } } - 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 7a3d1d5d..8ee4c662 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationStateService.cs @@ -384,4 +384,23 @@ public class ConversationStateService : IConversationStateService, IDisposable } 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/Files/Services/Storage/LocalFileStorageService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs index 44bfb9f1..d296e0b3 100644 --- a/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Files/Services/Storage/LocalFileStorageService.Conversation.cs @@ -69,6 +69,7 @@ public partial class LocalFileStorageService { MessageId = messageId, FileUrl = $"/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}", + FileDownloadUrl = $"/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download", FileStorageUrl = file, FileName = fileName, FileExtension = fileExtension, diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs index c2aac231..a5373cd1 100644 --- a/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs +++ b/src/Infrastructure/BotSharp.Core/Infrastructures/DistributedLocker.cs @@ -31,7 +31,7 @@ public class DistributedLocker } } - public async Task Lock(string resource, Func action, int timeoutInSeconds = 30) + public async Task Lock(string resource, Action action, int timeoutInSeconds = 30) { await ConnectToRedis(); @@ -45,7 +45,7 @@ public class DistributedLocker Serilog.Log.Logger.Error($"Acquire lock for {resource} failed due to after {timeout}s timeout."); } - return action(); + action(); } } diff --git a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs index f9b2efba..05efe39d 100644 --- a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs +++ b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs @@ -126,6 +126,12 @@ public class PluginLoader var plugins = GetPlugins(services); var pager = filter?.Pager ?? new Pagination(); + // Apply filter + if (!filter.Names.IsNullOrEmpty()) + { + plugins = plugins.Where(x => filter.Names.Any(n => x.Name.IsEqualTo(n))).ToList(); + } + return new PagedItems { Items = plugins.Skip(pager.Offset).Take(pager.Size), diff --git a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index 80c37f7a..d2c519eb 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 class BotSharpDbContext : Database, IBotSharpRepository 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 class BotSharpDbContext : Database, IBotSharpRepository 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 90f05ceb..59da45a5 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -1,5 +1,7 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Users.Models; +using Microsoft.Extensions.Logging; using System.IO; namespace BotSharp.Core.Repository @@ -188,7 +190,7 @@ namespace BotSharp.Core.Repository // Save default instructions var instructionFile = Path.Combine(instructionDir, $"{AGENT_INSTRUCTION_FILE}.{_agentSettings.TemplateFormat}"); File.WriteAllText(instructionFile, instruction ?? string.Empty); - Thread.Sleep(100); + Thread.Sleep(50); // Save channel instructions foreach (var ci in channelInstructions) @@ -197,7 +199,7 @@ namespace BotSharp.Core.Repository var file = Path.Combine(instructionDir, $"{AGENT_INSTRUCTION_FILE}.{ci.Channel}.{_agentSettings.TemplateFormat}"); File.WriteAllText(file, ci.Instruction ?? string.Empty); - Thread.Sleep(100); + Thread.Sleep(50); } } @@ -451,12 +453,63 @@ namespace BotSharp.Core.Repository public void BulkInsertAgents(List agents) { - _agents = []; + if (agents.IsNullOrEmpty()) return; + + var baseDir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir); + foreach (var agent in agents) + { + var dir = Path.Combine(baseDir, agent.Id); + if (Directory.Exists(dir)) continue; + + Directory.CreateDirectory(dir); + Thread.Sleep(50); + + var agentFile = Path.Combine(dir, AGENT_FILE); + var json = JsonSerializer.Serialize(agent, _options); + File.WriteAllText(agentFile, json); + + if (!string.IsNullOrWhiteSpace(agent.Instruction)) + { + var instDir = Path.Combine(dir, AGENT_INSTRUCTIONS_FOLDER); + Directory.CreateDirectory(instDir); + var instFile = Path.Combine(instDir, $"{AGENT_INSTRUCTION_FILE}.{_agentSettings.TemplateFormat}"); + File.WriteAllText(instFile, agent.Instruction); + } + } + Reset(); } public void BulkInsertUserAgents(List userAgents) { - _userAgents = []; + if (userAgents.IsNullOrEmpty()) return; + + var groups = userAgents.GroupBy(x => x.UserId); + var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER); + + foreach (var group in groups) + { + var filtered = group.Where(x => !string.IsNullOrEmpty(x.UserId) && !string.IsNullOrEmpty(x.AgentId)).ToList(); + if (filtered.IsNullOrEmpty()) continue; + + filtered.ForEach(x => x.Id = Guid.NewGuid().ToString()); + var userId = filtered.First().UserId; + var userDir = Path.Combine(usersDir, userId); + if (!Directory.Exists(userDir)) continue; + + var userAgentFile = Path.Combine(userDir, USER_AGENT_FILE); + var list = new List(); + if (File.Exists(userAgentFile)) + { + var str = File.ReadAllText(userAgentFile); + list = JsonSerializer.Deserialize>(str, _options); + } + + list.AddRange(filtered); + File.WriteAllText(userAgentFile, JsonSerializer.Serialize(list, _options)); + Thread.Sleep(50); + } + + Reset(); } public bool DeleteAgents() @@ -493,8 +546,7 @@ namespace BotSharp.Core.Repository // Delete agent folder Directory.Delete(agentDir, true); - _agents = []; - _userAgents = []; + Reset(); return true; } catch @@ -502,5 +554,11 @@ namespace BotSharp.Core.Repository return false; } } + + private void Reset() + { + _agents = []; + _userAgents = []; + } } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs index 7da6c849..28d0a6cc 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Conversation.cs @@ -57,6 +57,7 @@ namespace BotSharp.Core.Repository return true; } + [SideCar] public List GetConversationDialogs(string conversationId) { var dialogs = new List(); @@ -78,6 +79,7 @@ namespace BotSharp.Core.Repository return dialogs; } + [SideCar] public void AppendConversationDialogs(string conversationId, List dialogs) { var convDir = FindConversationDirectory(conversationId); @@ -182,6 +184,7 @@ namespace BotSharp.Core.Repository return true; } + [SideCar] public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { var convDir = FindConversationDirectory(conversationId); @@ -220,6 +223,7 @@ namespace BotSharp.Core.Repository } } + [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 46f39aaa..f3e1fddf 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.cs @@ -171,6 +171,8 @@ public partial class FileRepository : IBotSharpRepository } } + 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 93fa2f3b..f6b05375 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Planning/SequentialPlanner.cs @@ -147,7 +147,7 @@ public class SequentialPlanner : IRoutingPlaner 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 a77397d0..5c1f1903 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 class RoutingContext : IRoutingContext 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 class RoutingContext : IRoutingContext _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 ccd708c3..25ab3552 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 partial class RoutingService 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 partial class RoutingService 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 b7c489e1..770acaf9 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 int GetRecursiveCounter() + //{ + // return _currentRecursionDepth; + //} - public RoutingService(IServiceProvider services, + //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/Users/Services/UserIdentity.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs index 965be261..5f062a6f 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs @@ -70,18 +70,15 @@ public class UserIdentity : IUserIdentity [JsonPropertyName("phone")] public string? Phone => _claims?.FirstOrDefault(x => x.Type == "phone")?.Value; - [JsonPropertyName("affiliateId")] - public string? AffiliateId => _claims?.FirstOrDefault(x => x.Type == "affiliateId")?.Value; - - [JsonPropertyName("employeeId")] - public string? EmployeeId => _claims?.FirstOrDefault(x => x.Type == "employeeId")?.Value; - [JsonPropertyName("type")] public string? Type => _claims?.FirstOrDefault(x => x.Type == "type")?.Value; [JsonPropertyName("role")] public string? Role => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Role)?.Value; - [JsonPropertyName("regionCode")] - public string? RegionCode => _claims?.FirstOrDefault(x => x.Type == "regionCode")?.Value; + /// + /// US, CA, etc. + /// + [JsonPropertyName("region_code")] + public string? RegionCode => _claims?.FirstOrDefault(x => x.Type == "region_code")?.Value; } diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index 5bbd3c32..daf8a340 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -296,7 +296,7 @@ public class UserService : IUserService var (token, jwt) = BuildToken(record); foreach (var hook in hooks) { - hook.UserAuthenticated(jwt); + hook.UserAuthenticated(record, token); } return token; diff --git a/src/Infrastructure/BotSharp.Core/Using.cs b/src/Infrastructure/BotSharp.Core/Using.cs index e28eed72..8a0ca2af 100644 --- a/src/Infrastructure/BotSharp.Core/Using.cs +++ b/src/Infrastructure/BotSharp.Core/Using.cs @@ -33,6 +33,7 @@ global using BotSharp.Abstraction.Files.Utilities; 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/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 19f6241f..b7391be7 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,6 +1,7 @@ using Azure; using BotSharp.Abstraction.Files.Constants; using BotSharp.Abstraction.Files.Enums; +using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; using BotSharp.Abstraction.Users.Enums; @@ -488,6 +489,26 @@ public class ConversationController : ControllerBase } return BuildFileResult(file); } + + [HttpGet("/conversation/{conversationId}/message/{messageId}/{source}/file/{index}/{fileName}/download")] + public IActionResult DownloadMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string source, [FromRoute] string index, [FromRoute] string fileName) + { + var fileStorage = _services.GetRequiredService(); + var file = fileStorage.GetMessageFile(conversationId, messageId, source, index, fileName); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + + var fName = file.Split(Path.DirectorySeparatorChar).Last(); + var contentType = FileUtility.GetFileContentType(fName); + var stream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read); + var bytes = new byte[stream.Length]; + stream.Read(bytes, 0, (int)stream.Length); + stream.Position = 0; + + return new FileStreamResult(stream, contentType) { FileDownloadName = fName }; + } #endregion #region Private methods diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs index 787ab147..fa8ebab6 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Files/MessageFileViewModel.cs @@ -19,6 +19,10 @@ public class MessageFileViewModel [JsonPropertyName("file_source")] public string FileSource { get; set; } + [JsonPropertyName("file_download_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? FileDownloadUrl { get; set; } + public MessageFileViewModel() { @@ -32,7 +36,8 @@ public class MessageFileViewModel FileName = model.FileName, FileExtension = model.FileExtension, ContentType = model.ContentType, - FileSource = model.FileSource + FileSource = model.FileSource, + FileDownloadUrl = model.FileDownloadUrl }; } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index 4df12f8c..fae5873b 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -58,7 +58,7 @@ public class UserViewModel FirstName = user.FirstName, LastName = user.LastName, Email = user.Email, - Phone = user.Phone?.Substring(0, 3) == "+86" ? user.Phone.Substring(3) : user.Phone, + Phone = !string.IsNullOrWhiteSpace(user.Phone) ? user.Phone.Replace("+86", String.Empty) : user.Phone, Type = user.Type, Role = user.Role, Source = user.Source, diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Hooks/ChatHubConversationHook.cs index 29c6df82..b282fa53 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 class ChatHubConversationHook : ConversationHookBase public override async Task OnConversationInitialized(Conversation conversation) { + if (!AllowSendingMessage()) return; + var userService = _services.GetRequiredService(); var conv = ConversationViewModel.FromSession(conversation); @@ -44,6 +47,8 @@ public class ChatHubConversationHook : ConversationHookBase 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 class ChatHubConversationHook : ConversationHookBase 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 class ChatHubConversationHook : ConversationHookBase } #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.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs index ff986be6..47c646b7 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -37,6 +37,7 @@ public class WebSocketsMiddleware var regexes = new List { new Regex(@"/conversation/(.*?)/message/(.*?)/(.*?)/file/(.*?)/(.*?)", RegexOptions.IgnoreCase), + new Regex(@"/conversation/(.*?)/message/(.*?)/(.*?)/file/(.*?)/(.*?)/download", RegexOptions.IgnoreCase), new Regex(@"/user/avatar", RegexOptions.IgnoreCase), new Regex(@"/knowledge/document/(.*?)/file/(.*?)", RegexOptions.IgnoreCase) }; diff --git a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs index 35434e99..ebb5aa63 100644 --- a/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.KnowledgeBase/KnowledgeBasePlugin.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Settings; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Plugin.KnowledgeBase.Converters; using BotSharp.Plugin.KnowledgeBase.Hooks; using BotSharp.Plugin.KnowledgeBase.Services; @@ -25,7 +26,6 @@ public class KnowledgeBasePlugin : IBotSharpPlugin services.AddSingleton(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); } @@ -34,6 +34,7 @@ public class KnowledgeBasePlugin : IBotSharpPlugin var section = menu.First(x => x.Label == "Apps"); menu.Add(new PluginMenuDef("Knowledge Base", icon: "bx bx-book-open", weight: section.Weight + 1) { + Roles = new List { UserRole.Root, UserRole.Admin }, SubMenu = new List { new PluginMenuDef("Q & A", link: "page/knowledge-base/question-answer"), diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index d4a02e93..44a4e2bc 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -429,10 +429,13 @@ public partial class MongoRepository { if (userAgents.IsNullOrEmpty()) return; - var userAgentDocs = userAgents.Select(x => new UserAgentDocument + var filtered = userAgents.Where(x => !string.IsNullOrEmpty(x.UserId) && !string.IsNullOrEmpty(x.AgentId)).ToList(); + if (filtered.IsNullOrEmpty()) return; + + var userAgentDocs = filtered.Select(x => new UserAgentDocument { Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), - UserId = !string.IsNullOrEmpty(x.UserId) ? x.UserId : string.Empty, + UserId = x.UserId, AgentId = x.AgentId, Actions = x.Actions, CreatedTime = x.CreatedTime, diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Conversation.cs index d8159da1..85fa1033 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 partial class MongoRepository || contentLogDeleted.DeletedCount > 0 || stateLogDeleted.DeletedCount > 0; } + [SideCar] public List GetConversationDialogs(string conversationId) { var dialogs = new List(); @@ -83,6 +84,7 @@ public partial class MongoRepository return formattedDialog ?? new List(); } + [SideCar] public void AppendConversationDialogs(string conversationId, List dialogs) { if (string.IsNullOrEmpty(conversationId)) return; @@ -159,6 +161,7 @@ public partial class MongoRepository return true; } + [SideCar] public void UpdateConversationBreakpoint(string conversationId, ConversationBreakpoint breakpoint) { if (string.IsNullOrEmpty(conversationId)) return; @@ -176,6 +179,7 @@ public partial class MongoRepository _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 689c06be..258c1883 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -22,4 +22,6 @@ public partial class MongoRepository : IBotSharpRepository 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 7c74b648..13b2739b 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.Agents.Enums; 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 2ecd9ac0..565115a5 100644 --- a/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs +++ b/src/Plugins/BotSharp.Plugin.Planner/TwoStaging/TwoStageTaskPlanner.cs @@ -92,7 +92,7 @@ public partial class TwoStageTaskPlanner : IRoutingPlaner } var routing = _services.GetRequiredService(); - routing.ResetRecursiveCounter(); + routing.Context.ResetRecursiveCounter(); return true; } diff --git a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs index 844afb65..69cda8ca 100644 --- a/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs +++ b/src/Plugins/BotSharp.Plugin.TencentCos/Services/TencentCosService.Conversation.cs @@ -66,6 +66,7 @@ public partial class TencentCosService { MessageId = messageId, FileUrl = BuilFileUrl(file), + FileDownloadUrl = BuilFileUrl(file), FileStorageUrl = file, FileName = fileName, FileExtension = fileExtension, diff --git a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs index 108ff433..9b00b010 100644 --- a/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs +++ b/src/Plugins/BotSharp.Plugin.WebDriver/Drivers/PlaywrightDriver/PlaywrightWebDriver.GoToPage.cs @@ -1,3 +1,5 @@ +using Microsoft.Playwright; + namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver; public partial class PlaywrightWebDriver @@ -47,15 +49,37 @@ public partial class PlaywrightWebDriver var response = await page.GotoAsync(args.Url, new PageGotoOptions { - Timeout = args.Timeout + Timeout = args.Timeout > 0 ? args.Timeout : 30000 }); + if (args.Selectors != null) + { + // 使用传入的选择器列表进行并行等待 + var tasks =args.Selectors.Select(selector => + page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions + { + Timeout = args.Timeout > 0 ? args.Timeout : 30000 + }) + ).ToArray(); + + await Task.WhenAll(tasks); + + // 在此处提取所有选择器的 HTML 内容 + var contentTasks = args.Selectors.Select(selector => page.InnerHTMLAsync(selector)).ToArray(); + var contents = await Task.WhenAll(contentTasks); + + result.IsSuccess = true; + result.Body = string.Join(", ", contents.Select((content, index) => $"{args.Selectors[index]}: {content}")); + + return result; + } + await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); if (args.WaitForNetworkIdle) { await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions { - Timeout = args.Timeout + Timeout = args.Timeout > 0 ? args.Timeout : 30000 }); } diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 4bd62176..a8f819d6 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 7f43ba28..8c4cd9aa 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",