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/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index 1fd7a38c..1461cc22 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -55,7 +55,7 @@ public interface IAgentService string GetDataDir(); string GetAgentDataDir(string agentId); - List GetAgentsByUser(string userId); + Task> GetUserAgents(string userId); PluginDef GetPlugin(string agentId); diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/UserAgent.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/UserAgent.cs index b6eab94d..a130f9b2 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/UserAgent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/UserAgent.cs @@ -1,11 +1,27 @@ +using BotSharp.Abstraction.Users.Models; + namespace BotSharp.Abstraction.Agents.Models; public class UserAgent { + [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + + [JsonPropertyName("user_id")] public string UserId { get; set; } = string.Empty; - public string AgentId { get; set; } = string.Empty; - public bool Editable { get; set; } + + [JsonPropertyName("agent_id")] + public string AgentId { get; set; } + + [JsonPropertyName("actions")] + public IEnumerable Actions { get; set; } = []; + + [JsonIgnore] + public Agent? Agent { get; set; } + + [JsonPropertyName("updated_time")] public DateTime UpdatedTime { get; set; } = DateTime.UtcNow; + + [JsonPropertyName("created_time")] public DateTime CreatedTime { get; set; } = DateTime.UtcNow; } 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/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/Repositories/IBotSharpRepository.cs b/src/Infrastructure/BotSharp.Abstraction/Repositories/IBotSharpRepository.cs index 003eddf4..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,11 +9,8 @@ using BotSharp.Abstraction.VectorStorage.Models; namespace BotSharp.Abstraction.Repositories; -public interface IBotSharpRepository +public interface IBotSharpRepository : IHaveServiceProvider { - int Transaction(Action action); - void Add(object entity); - #region Plugin PluginConfig GetPluginConfig(); void SavePluginConfig(PluginConfig config); @@ -35,13 +33,15 @@ public interface IBotSharpRepository void UpdateUserPhone(string userId, string Iphone, string regionCode) => throw new NotImplementedException(); void UpdateUserIsDisable(string userId, bool isDisable) => throw new NotImplementedException(); void UpdateUsersIsDisable(List userIds, bool isDisable) => throw new NotImplementedException(); + PagedItems GetUsers(UserFilter filter) => throw new NotImplementedException(); + bool UpdateUser(User user, bool isUpdateUserAgents = false) => throw new NotImplementedException(); #endregion #region Agent void UpdateAgent(Agent agent, AgentField field); Agent? GetAgent(string agentId); List GetAgents(AgentFilter filter); - List GetAgentsByUser(string userId); + List GetUserAgents(string userId); void BulkInsertAgents(List agents); void BulkInsertUserAgents(List userAgents); bool DeleteAgents(); 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/Enums/UserAction.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs new file mode 100644 index 00000000..4838e757 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserAction.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Abstraction.Users.Enums; + +public static class UserAction +{ + public const string Edit = "edit"; + public const string Chat = "chat"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserConstant.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserConstant.cs new file mode 100644 index 00000000..9fde5376 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserConstant.cs @@ -0,0 +1,10 @@ +namespace BotSharp.Abstraction.Users.Enums; + +public static class UserConstant +{ + public static IEnumerable AdminRoles = new List + { + UserRole.Admin, + UserRole.Root + }; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs new file mode 100644 index 00000000..58dfc186 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserPermission.cs @@ -0,0 +1,6 @@ +namespace BotSharp.Abstraction.Users.Enums; + +public static class UserPermission +{ + public const string CreateAgent = "create-agent"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs index cddabe10..0bde3b08 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Enums/UserRole.cs @@ -35,4 +35,4 @@ public class UserRole public const string Assistant = "assistant"; public const string Root = "root"; -} +} \ No newline at end of file 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.Abstraction/Users/IUserService.cs b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs index 8123b272..2a869d02 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/IUserService.cs @@ -6,6 +6,8 @@ namespace BotSharp.Abstraction.Users; public interface IUserService { Task GetUser(string id); + Task> GetUsers(UserFilter filter); + Task UpdateUser(User model, bool isUpdateUserAgents = false); Task CreateUser(User user); Task ActiveUser(UserActivationModel model); Task GetAffiliateToken(string authorization); diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs index 0baa7457..8d136dc5 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/User.cs @@ -25,6 +25,10 @@ public class User public string? AffiliateId { get; set; } public string? EmployeeId { get; set; } public bool IsDisabled { get; set; } + public IEnumerable Permissions { get; set; } = []; + + [JsonIgnore] + public IEnumerable AgentActions { get; set; } = []; public DateTime UpdatedTime { get; set; } = DateTime.UtcNow; public DateTime CreatedTime { get; set; } = DateTime.UtcNow; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAgentAction.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAgentAction.cs new file mode 100644 index 00000000..1ac759a1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserAgentAction.cs @@ -0,0 +1,16 @@ +namespace BotSharp.Abstraction.Users.Models; + +public class UserAgentAction +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("agent_id")] + public string AgentId { get; set; } + + [JsonIgnore] + public Agent? Agent { get; set; } + + [JsonPropertyName("actions")] + public IEnumerable Actions { get; set; } = []; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserFilter.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserFilter.cs new file mode 100644 index 00000000..73e1794d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/UserFilter.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Abstraction.Users.Models; + +public class UserFilter : Pagination +{ + [JsonPropertyName("user_ids")] + public IEnumerable? UserIds { get; set; } + + [JsonPropertyName("user_names")] + public IEnumerable? UserNames { get; set; } + + [JsonPropertyName("external_ids")] + public IEnumerable? ExternalIds { get; set; } + + [JsonPropertyName("roles")] + public IEnumerable? Roles { get; set; } + + [JsonPropertyName("sources")] + public IEnumerable? Sources { get; set; } +} 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/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index 43bd9c7f..2f51f1dd 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -45,7 +45,7 @@ public class AgentPlugin : IBotSharpPlugin SubMenu = new List { new PluginMenuDef("Routing", link: "page/agent/router"), // icon: "bx bx-map-pin" - new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = new List { UserRole.Admin } }, // icon: "bx bx-task" + new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = new List { UserRole.Root, UserRole.Admin } }, // icon: "bx bx-task" new PluginMenuDef("Agents", link: "page/agent"), // icon: "bx bx-bot" } }); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs index 9e2e1d5f..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; @@ -8,14 +9,14 @@ public partial class AgentService { public async Task CreateAgent(Agent agent) { - var agentRecord = _db.GetAgentsByUser(_user.Id).FirstOrDefault(x => x.Name.IsEqualTo(agent.Name)); - - if (agentRecord != null) + var userAgents = _db.GetUserAgents(_user.Id); + var found = userAgents?.FirstOrDefault(x => x.Agent != null && x.Agent.Name.IsEqualTo(agent.Name)); + if (found != null) { - return agentRecord; + return found.Agent; } - agentRecord = Agent.Clone(agent); + var agentRecord = Agent.Clone(agent); agentRecord.Id = Guid.NewGuid().ToString(); agentRecord.CreatedDateTime = DateTime.UtcNow; agentRecord.UpdatedDateTime = DateTime.UtcNow; @@ -24,21 +25,21 @@ public partial class AgentService var agentSettings = _services.GetRequiredService(); var user = _db.GetUserById(_user.Id); - var userAgentRecord = new UserAgent + _db.BulkInsertAgents(new List { agentRecord }); + if (!UserConstant.AdminRoles.Contains(user.Role)) { - Id = Guid.NewGuid().ToString(), - UserId = user.Id, - AgentId = agentRecord.Id, - Editable = false, - CreatedTime = DateTime.UtcNow, - UpdatedTime = DateTime.UtcNow - }; - - _db.Transaction(delegate - { - _db.Add(agentRecord); - _db.Add(userAgentRecord); - }); + _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); @@ -213,17 +214,4 @@ public partial class AgentService task.Content = content.Substring(suffix.Length).Trim(); return task; } - - private UserAgent BuildUserAgent(string agentId, string userId, bool editable = false) - { - return new UserAgent - { - Id = Guid.NewGuid().ToString(), - UserId = userId, - AgentId = agentId, - Editable = editable, - CreatedTime = DateTime.UtcNow, - UpdatedTime = DateTime.UtcNow - }; - } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs index 1fe5c6e1..8a05542e 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs @@ -7,9 +7,10 @@ public partial class AgentService public async Task DeleteAgent(string id) { var user = _db.GetUserById(_user.Id); - var agent = _db.GetAgentsByUser(_user.Id).FirstOrDefault(x => x.Id.IsEqualTo(id)); + var userAgents = await GetUserAgents(user?.Id); + var found = userAgents?.FirstOrDefault(x => x.AgentId == id); - if (user?.Role != UserRole.Admin && agent == null) + if (!UserConstant.AdminRoles.Contains(user?.Role) && (found?.Actions == null || !found.Actions.Contains(UserAction.Edit))) { return false; } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs index 10598d4e..e4a311d2 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.RefreshAgents.cs @@ -54,7 +54,6 @@ public partial class AgentService .SetResponses(responses) .SetSamples(samples); - var userAgent = BuildUserAgent(agent.Id, user.Id); var tasks = GetTasksFromFile(dir); var isAgentDeleted = _db.DeleteAgent(agent.Id); @@ -62,7 +61,6 @@ public partial class AgentService { await Task.Delay(100); _db.BulkInsertAgents(new List { agent }); - _db.BulkInsertUserAgents(new List { userAgent }); _db.BulkInsertAgentTasks(tasks); refreshedAgents.Add(agent.Name); _logger.LogInformation($"Agent {agent.Name} has been migrated."); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index fe532e28..17aa4aa7 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -9,13 +9,18 @@ public partial class AgentService { public async Task UpdateAgent(Agent agent, AgentField updateField) { + if (agent == null || string.IsNullOrEmpty(agent.Id)) return; + var userService = _services.GetRequiredService(); var user = await userService.GetUser(_user.Id); - var userAgents = GetAgentsByUser(user?.Id); - var editable = userAgents?.Select(x => x.Id)?.Contains(agent.Id) ?? false; - if (user?.Role != UserRole.Admin && !editable) return; - if (agent == null || string.IsNullOrEmpty(agent.Id)) return; + var userAgents = await GetUserAgents(user.Id); + var found = userAgents?.FirstOrDefault(x => x.AgentId == agent.Id); + + if (!UserConstant.AdminRoles.Contains(user?.Role) && (found?.Actions == null || found.Actions.Contains(UserAction.Edit))) + { + return; + } var record = _db.GetAgent(agent.Id); if (record == null) return; diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index b69f7529..a48201d8 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -49,10 +49,12 @@ public partial class AgentService : IAgentService return dir; } - public List GetAgentsByUser(string userId) + public async Task> GetUserAgents(string userId) { - var agents = _db.GetAgentsByUser(userId); - return agents; + if (string.IsNullOrEmpty(userId)) return []; + + var userAgents = _db.GetUserAgents(userId); + return userAgents; } public IEnumerable GetAgentUtilities() 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/Repository/BotSharpDbContext.cs b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs index a4131e64..d2c519eb 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/BotSharpDbContext.cs @@ -2,70 +2,13 @@ using BotSharp.Abstraction.Loggers.Models; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Tasks.Models; using BotSharp.Abstraction.Translation.Models; -using BotSharp.Abstraction.Users.Models; using BotSharp.Abstraction.VectorStorage.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; namespace BotSharp.Core.Repository; public class BotSharpDbContext : Database, IBotSharpRepository { - public IQueryable Users => throw new NotImplementedException(); - - public IQueryable Agents => throw new NotImplementedException(); - - public IQueryable UserAgents => throw new NotImplementedException(); - - public IQueryable Conversations => throw new NotImplementedException(); - - public int Transaction(Action action) - { - DatabaseFacade database = base.GetMaster(typeof(TTableInterface)).Database; - int num = 0; - if (database.CurrentTransaction == null) - { - using (Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction dbContextTransaction = database.BeginTransaction()) - { - try - { - action(); - num = base.SaveChanges(); - dbContextTransaction.Commit(); - return num; - } - catch (Exception ex) - { - dbContextTransaction.Rollback(); - if (ex.Message.Contains("See the inner exception for details")) - { - throw ex.InnerException; - } - - throw ex; - } - } - } - - try - { - action(); - return base.SaveChanges(); - } - catch (Exception ex2) - { - if (database.CurrentTransaction != null) - { - database.CurrentTransaction.Rollback(); - } - - if (ex2.Message.Contains("See the inner exception for details")) - { - throw ex2.InnerException; - } - - throw ex2; - } - } + public IServiceProvider ServiceProvider => throw new NotImplementedException(); #region Plugin public PluginConfig GetPluginConfig() => throw new NotImplementedException(); @@ -79,7 +22,7 @@ public class BotSharpDbContext : Database, IBotSharpRepository public List GetAgents(AgentFilter filter) => throw new NotImplementedException(); - public List GetAgentsByUser(string userId) + public List GetUserAgents(string userId) => throw new NotImplementedException(); public void UpdateAgent(Agent agent, AgentField field) @@ -149,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(); @@ -167,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 bb137279..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.Functions.Models; +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); } } @@ -386,19 +388,26 @@ namespace BotSharp.Core.Repository return query.ToList(); } - public List GetAgentsByUser(string userId) + public List GetUserAgents(string userId) { - var agentIds = (from ua in UserAgents - join u in Users on ua.UserId equals u.Id - where ua.UserId == userId || u.ExternalId == userId - select ua.AgentId).ToList(); + var found = (from ua in UserAgents + join u in Users on ua.UserId equals u.Id + where ua.UserId == userId || u.ExternalId == userId + select ua).ToList(); - var filter = new AgentFilter + if (found.IsNullOrEmpty()) return []; + + var agentIds = found.Select(x => x.AgentId).Distinct().ToList(); + var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); + foreach (var item in found) { - AgentIds = agentIds - }; - var agents = GetAgents(filter); - return agents; + var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); + if (agent == null) continue; + + item.Agent = agent; + } + + return found; } @@ -442,9 +451,66 @@ namespace BotSharp.Core.Repository return true; } - public void BulkInsertAgents(List agents) { } + public void BulkInsertAgents(List agents) + { + if (agents.IsNullOrEmpty()) return; - public void BulkInsertUserAgents(List userAgents) { } + 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) + { + 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() { @@ -473,14 +539,14 @@ namespace BotSharp.Core.Repository var userAgents = JsonSerializer.Deserialize>(text, _options); if (userAgents.IsNullOrEmpty()) continue; - userAgents = userAgents.Where(x => x.AgentId != agentId).ToList(); + userAgents = userAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? []; File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options)); } } // Delete agent folder Directory.Delete(agentDir, true); - + Reset(); return true; } catch @@ -488,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.Transaction.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Transaction.cs deleted file mode 100644 index 13152f93..00000000 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Transaction.cs +++ /dev/null @@ -1,82 +0,0 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Users.Models; -using System.IO; - -namespace BotSharp.Core.Repository; - -public partial class FileRepository -{ - public void Add(object entity) - { - if (entity is Agent agent) - { - _agents.Add(agent); - _changedTableNames.Add(nameof(Agent)); - } - else if (entity is User user) - { - _users.Add(user); - _changedTableNames.Add(nameof(User)); - } - else if (entity is UserAgent userAgent) - { - _userAgents.Add(userAgent); - _changedTableNames.Add(nameof(UserAgent)); - } - } - - private readonly List _changedTableNames = new List(); - public int Transaction(Action action) - { - _changedTableNames.Clear(); - action(); - - // Persist to disk - foreach (var table in _changedTableNames) - { - if (table == nameof(Agent)) - { - foreach (var agent in _agents) - { - var dir = Path.Combine(_dbSettings.FileRepository, _agentSettings.DataDir, agent.Id); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - var path = Path.Combine(dir, AGENT_FILE); - File.WriteAllText(path, JsonSerializer.Serialize(agent, _options)); - } - } - else if (table == nameof(User)) - { - foreach (var user in _users) - { - var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, user.Id); - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - var path = Path.Combine(dir, USER_FILE); - File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); - } - } - else if (table == nameof(UserAgent)) - { - _userAgents.GroupBy(x => x.UserId) - .Select(x => x.Key).ToList() - .ForEach(uid => - { - var agents = _userAgents.Where(x => x.UserId == uid).ToList(); - if (agents.Any()) - { - var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, uid); - var path = Path.Combine(dir, USER_AGENT_FILE); - File.WriteAllText(path, JsonSerializer.Serialize(agents, _options)); - } - }); - } - } - - return _changedTableNames.Count; - } -} diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index b7c8aae6..80ccacc4 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -1,5 +1,7 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; +using System; using System.IO; namespace BotSharp.Core.Repository; @@ -68,4 +70,100 @@ public partial class FileRepository var path = Path.Combine(dir, USER_FILE); File.WriteAllText(path, JsonSerializer.Serialize(user, _options)); } + + public PagedItems GetUsers(UserFilter filter) + { + var users = Users; + + // Apply filters + if (!filter.UserIds.IsNullOrEmpty()) + { + users = users.Where(x => filter.UserIds.Contains(x.Id)); + } + if (!filter.UserNames.IsNullOrEmpty()) + { + users = users.Where(x => filter.UserNames.Contains(x.UserName)); + } + if (!filter.ExternalIds.IsNullOrEmpty()) + { + users = users.Where(x => filter.ExternalIds.Contains(x.ExternalId)); + } + if (!filter.Roles.IsNullOrEmpty()) + { + users = users.Where(x => filter.Roles.Contains(x.Role)); + } + if (!filter.Sources.IsNullOrEmpty()) + { + users = users.Where(x => filter.Sources.Contains(x.Source)); + } + + // Get user agents + var userIds = users.Select(x => x.Id).ToList(); + var userAgents = UserAgents.Where(x => userIds.Contains(x.UserId)).ToList(); + var agentIds = userAgents?.Select(x => x.AgentId)?.Distinct()?.ToList() ?? []; + + if (!agentIds.IsNullOrEmpty()) + { + var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); + foreach (var item in userAgents) + { + item.Agent = agents.FirstOrDefault(x => x.Id == item.AgentId); + } + + foreach (var user in users) + { + var found = userAgents.Where(x => x.UserId == user.Id).ToList(); + if (found.IsNullOrEmpty()) continue; + + user.AgentActions = found.Select(x => new UserAgentAction + { + Id = x.Id, + AgentId = x.AgentId, + Agent = x.Agent, + Actions = x.Actions + }); + } + } + + return new PagedItems + { + Items = users.OrderByDescending(x => x.CreatedTime).Skip(filter.Offset).Take(filter.Size), + Count = users.Count() + }; + } + + public bool UpdateUser(User user, bool isUpdateUserAgents = false) + { + if (string.IsNullOrEmpty(user?.Id)) return false; + + var dir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER, user.Id); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var userFile = Path.Combine(dir, USER_FILE); + user.UpdatedTime = DateTime.UtcNow; + File.WriteAllText(userFile, JsonSerializer.Serialize(user, _options)); + + if (isUpdateUserAgents) + { + var userAgents = user.AgentActions?.Select(x => new UserAgent + { + Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), + UserId = user.Id, + AgentId = x.AgentId, + Actions = x.Actions ?? [], + CreatedTime = DateTime.UtcNow, + UpdatedTime = DateTime.UtcNow + })?.ToList() ?? []; + + var userAgentFile = Path.Combine(dir, USER_AGENT_FILE); + File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options)); + } + + _users = []; + _userAgents = []; + return true; + } } 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/Tasks/TaskPlugin.cs b/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs index 27c55ab5..7f907fd1 100644 --- a/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs @@ -22,7 +22,7 @@ public class TaskPlugin : IBotSharpPlugin var section = menu.First(x => x.Label == "Apps"); menu.Add(new PluginMenuDef("Task", link: "page/task", icon: "bx bx-task", weight: section.Weight + 8) { - Roles = new List { UserRole.Admin } + Roles = new List { UserRole.Root, UserRole.Admin } }); return true; 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 f7f4a6de..5bbd3c32 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -400,6 +400,19 @@ public class UserService : IUserService return user; } + public async Task> GetUsers(UserFilter filter) + { + var db = _services.GetRequiredService(); + var users = db.GetUsers(filter); + return users; + } + + public async Task UpdateUser(User model, bool isUpdateUserAgents = false) + { + var db = _services.GetRequiredService(); + return db.UpdateUser(model, isUpdateUserAgents); + } + public async Task ActiveUser(UserActivationModel model) { var id = model.UserName; 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/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index f033e31c..54c9b75e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Users.Enums; namespace BotSharp.OpenAPI.Controllers; @@ -58,24 +59,64 @@ public class AgentController : ControllerBase } var editable = true; + var chatable = true; var userService = _services.GetRequiredService(); var user = await userService.GetUser(_user.Id); - if (user?.Role != UserRole.Admin) + if (!UserConstant.AdminRoles.Contains(user?.Role)) { - var userAgents = _agentService.GetAgentsByUser(user?.Id); - editable = userAgents?.Select(x => x.Id)?.Contains(targetAgent.Id) ?? false; + var userAgents = await _agentService.GetUserAgents(user?.Id); + var actions = userAgents?.FirstOrDefault(x => x.AgentId == targetAgent.Id)?.Actions ?? []; + editable = actions.Contains(UserAction.Edit); + chatable = actions.Contains(UserAction.Chat); } targetAgent.Editable = editable; + targetAgent.Chatable = chatable; return targetAgent; } [HttpGet("/agents")] - public async Task> GetAgents([FromQuery] AgentFilter filter) + public async Task> GetAgents([FromQuery] AgentFilter filter, [FromQuery] bool checkAuth = false) { var agentSetting = _services.GetRequiredService(); + var userService = _services.GetRequiredService(); + + List agents; var pagedAgents = await _agentService.GetAgents(filter); - var agents = pagedAgents?.Items?.Select(x => AgentViewModel.FromAgent(x))?.ToList() ?? new List(); + + if (!checkAuth) + { + agents = pagedAgents?.Items?.Select(x => AgentViewModel.FromAgent(x))?.ToList() ?? []; + return new PagedItems + { + Items = agents, + Count = pagedAgents?.Count ?? 0 + }; + } + + var userAgents = new List(); + var user = await userService.GetUser(_user.Id); + if (!UserConstant.AdminRoles.Contains(user.Role)) + { + userAgents = await _agentService.GetUserAgents(user.Id); + } + + agents = pagedAgents?.Items?.Select(x => + { + var chatable = true; + var editable = true; + if (!UserConstant.AdminRoles.Contains(user.Role)) + { + var actions = userAgents.FirstOrDefault(a => a.AgentId == x.Id)?.Actions ?? []; + chatable = actions.Contains(UserAction.Chat); + editable = actions.Contains(UserAction.Edit); + } + + var model = AgentViewModel.FromAgent(x); + model.Editable = editable; + model.Chatable = chatable; + return model; + })?.ToList() ?? []; return new PagedItems { diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 91617a0f..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; @@ -55,7 +56,7 @@ public class ConversationController : ControllerBase return new PagedItems(); } - filter.UserId = user.Role != UserRole.Admin ? user.Id : filter.UserId; + filter.UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : filter.UserId; var conversations = await convService.GetConversations(filter); var agentService = _services.GetRequiredService(); var list = conversations.Items.Select(x => ConversationViewModel.FromSession(x)).ToList(); @@ -146,7 +147,7 @@ public class ConversationController : ControllerBase var filter = new ConversationFilter { Id = conversationId, - UserId = user.Role != UserRole.Admin ? user.Id : null + UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : null }; var conversations = await service.GetConversations(filter); if (conversations.Items.IsNullOrEmpty()) @@ -209,7 +210,7 @@ public class ConversationController : ControllerBase var filter = new ConversationFilter { Id = conversationId, - UserId = user.Role != UserRole.Admin ? user.Id : null + UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : null }; var conversations = await conv.GetConversations(filter); @@ -262,7 +263,7 @@ public class ConversationController : ControllerBase var filter = new ConversationFilter { Id = conversationId, - UserId = user.Role != UserRole.Admin ? user.Id : null + UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : null }; var conversations = await conversationService.GetConversations(filter); @@ -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/Controllers/PluginController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs index 342f39fb..e4ec4aa8 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs @@ -24,7 +24,7 @@ public class PluginController : ControllerBase { var userService = _services.GetRequiredService(); var user = await userService.GetUser(_user.Id); - if (user?.Role != UserRole.Admin) + if (!UserConstant.AdminRoles.Contains(user?.Role)) { return new PagedItems(); } @@ -45,15 +45,19 @@ public class PluginController : ControllerBase new PluginMenuDef("System", weight: 30) { IsHeader = true, - Roles = new List { UserRole.Admin } + Roles = new List { UserRole.Root, UserRole.Admin } }, new PluginMenuDef("Plugins", link: "page/plugin", icon: "bx bx-plug", weight: 31) { - Roles = new List { UserRole.Admin } + Roles = new List { UserRole.Root, UserRole.Admin } }, new PluginMenuDef("Settings", link: "page/setting", icon: "bx bx-cog", weight: 32) { - Roles = new List { UserRole.Admin } + Roles = new List { UserRole.Root, UserRole.Admin } + }, + new PluginMenuDef("Users", link: "page/users", icon: "bx bx-user", weight: 33) + { + Roles = new List { UserRole.Root, UserRole.Admin } } }; diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index de31ef6b..90bf20f1 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Users.Settings; +using BotSharp.Abstraction.Users.Enums; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using System.ComponentModel.DataAnnotations; @@ -11,11 +12,18 @@ public class UserController : ControllerBase { private readonly IServiceProvider _services; private readonly IUserService _userService; + private readonly IUserIdentity _user; private readonly AccountSetting _setting; - public UserController(IUserService userService, IServiceProvider services, AccountSetting setting) + + public UserController( + IUserService userService, + IServiceProvider services, + IUserIdentity user, + AccountSetting setting) { _services = services; _userService = userService; + _user = user; _setting = setting; } @@ -169,6 +177,46 @@ public class UserController : ControllerBase return await _userService.UpdateUsersIsDisable(userIds, isDisable); } + #region User management + [HttpPost("/users")] + public async Task> GetUsers([FromBody] UserFilter filter) + { + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user == null || !UserConstant.AdminRoles.Contains(user.Role)) + { + return new PagedItems(); + } + + var users = await userService.GetUsers(filter); + var views = users.Items.Select(x => UserViewModel.FromUser(x)).ToList(); + + return new PagedItems + { + Count = users.Count, + Items = views + }; + } + + + [HttpPut("/user")] + public async Task UpdateUser([FromBody] UserUpdateModel model) + { + if (model == null) return false; + + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user == null || !UserConstant.AdminRoles.Contains(user.Role)) + { + return false; + } + + var updated = await userService.UpdateUser(UserUpdateModel.ToUser(model), isUpdateUserAgents: true); + return updated; + } + #endregion + + #region Avatar [HttpPost("/user/avatar")] public bool UploadUserAvatar([FromBody] UserAvatarModel input) diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs index 5c23c4d4..b368cde0 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs @@ -47,6 +47,7 @@ public class AgentViewModel public PluginDef Plugin { get; set; } public bool Editable { get; set; } + public bool Chatable { get; set; } [JsonPropertyName("created_datetime")] public DateTime CreatedDateTime { get; set; } 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/UserAgentActionViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAgentActionViewModel.cs new file mode 100644 index 00000000..43c9fc4d --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserAgentActionViewModel.cs @@ -0,0 +1,40 @@ +using BotSharp.Abstraction.Agents.Models; +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Users; + +public class UserAgentActionViewModel +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("agent_id")] + public string AgentId { get; set; } + + [JsonPropertyName("agent")] + public Agent? Agent { get; set; } + + [JsonPropertyName("actions")] + public IEnumerable Actions { get; set; } = []; + + public static UserAgentActionViewModel ToViewModel(UserAgentAction action) + { + return new UserAgentActionViewModel + { + Id = action.Id, + AgentId = action.AgentId, + Agent = action.Agent, + Actions = action.Actions + }; + } + + public static UserAgentAction ToDomainModel(UserAgentActionViewModel action) + { + return new UserAgentAction + { + Id = action.Id, + AgentId = action.AgentId, + Actions = action.Actions + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs new file mode 100644 index 00000000..76b8e347 --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserUpdateModel.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Users; + +public class UserUpdateModel +{ + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("user_name")] + public string UserName { get; set; } = string.Empty; + + [JsonPropertyName("first_name")] + public string FirstName { get; set; } = string.Empty; + + [JsonPropertyName("last_name")] + public string? LastName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public string? Type { get; set; } + public string? Role { get; set; } + public string? Source { get; set; } + + [JsonPropertyName("external_id")] + public string? ExternalId { get; set; } + + public IEnumerable Permissions { get; set; } = []; + + [JsonPropertyName("agent_actions")] + public IEnumerable AgentActions { get; set; } = []; + + public static User ToUser(UserUpdateModel model) + { + return new User + { + Id = model.Id, + UserName = model.UserName, + FirstName = model.FirstName, + LastName = model.LastName, + Email = model.Email, + Phone = model.Phone, + Type = model.Type, + Role = model.Role, + Source = model.Source, + ExternalId = model.ExternalId, + Permissions = model.Permissions, + AgentActions = model.AgentActions?.Select(x => UserAgentActionViewModel.ToDomainModel(x)) ?? [] + }; + } +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index c2c5afd5..4dea311f 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -16,14 +16,23 @@ public class UserViewModel public string? Phone { get; set; } public string Type { get; set; } = UserType.Client; public string Role { get; set; } = UserRole.User; + [JsonPropertyName("full_name")] public string FullName => $"{FirstName} {LastName}".Trim(); public string? Source { get; set; } + [JsonPropertyName("external_id")] public string? ExternalId { get; set; } public string Avatar { get; set; } = "/user/avatar"; + + public IEnumerable Permissions { get; set; } = []; + + [JsonPropertyName("agent_actions")] + public IEnumerable AgentActions { get; set; } = []; + [JsonPropertyName("create_date")] public DateTime CreateDate { get; set; } + [JsonPropertyName("update_date")] public DateTime UpdateDate { get; set; } @@ -54,6 +63,8 @@ public class UserViewModel Role = user.Role, Source = user.Source, ExternalId = user.ExternalId, + Permissions = user.Permissions, + AgentActions = user.AgentActions?.Select(x => UserAgentActionViewModel.ToViewModel(x)) ?? [], CreateDate = user.CreatedTime, UpdateDate = user.UpdatedTime, Avatar = "/user/avatar", 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.MongoStorage/Collections/UserAgentDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserAgentDocument.cs index bb4af1ae..9de5345f 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserAgentDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserAgentDocument.cs @@ -4,8 +4,7 @@ public class UserAgentDocument : MongoBase { public string UserId { get; set; } public string AgentId { get; set; } - public bool Editable { get; set; } - + public IEnumerable Actions { get; set; } = []; public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs index c6f42eab..4054d09d 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/UserDocument.cs @@ -22,6 +22,7 @@ public class UserDocument : MongoBase public string? AffiliateId { get; set; } public string? EmployeeId { get; set; } public bool IsDisabled { get; set; } + public IEnumerable Permissions { get; set; } = []; public DateTime CreatedTime { get; set; } public DateTime UpdatedTime { get; set; } @@ -47,6 +48,7 @@ public class UserDocument : MongoBase VerificationCode = VerificationCode, Verified = Verified, RegionCode = RegionCode, + Permissions = Permissions, }; } } \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs index 7d47a98c..755958c7 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/MongoStoragePlugin.cs @@ -37,7 +37,7 @@ public class MongoStoragePlugin : IBotSharpPlugin var section = menu.First(x => x.Label == "Apps"); menu.Add(new PluginMenuDef("MongoDB", icon: "bx bx-data", link: "page/mongodb", weight: section.Weight + 10) { - Roles = new List { UserRole.Admin } + Roles = new List { UserRole.Root, UserRole.Admin } }); return true; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index 49c8616e..44a4e2bc 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -318,19 +318,36 @@ public partial class MongoRepository return agentDocs.Select(x => TransformAgentDocument(x)).ToList(); } - public List GetAgentsByUser(string userId) + public List GetUserAgents(string userId) { - var agentIds = (from ua in _dc.UserAgents.AsQueryable() - join u in _dc.Users.AsQueryable() on ua.UserId equals u.Id - where ua.UserId == userId || u.ExternalId == userId - select ua.AgentId).ToList(); + var found = (from ua in _dc.UserAgents.AsQueryable() + join u in _dc.Users.AsQueryable() on ua.UserId equals u.Id + where ua.UserId == userId || u.ExternalId == userId + select ua).ToList(); - var filter = new AgentFilter + if (found.IsNullOrEmpty()) return []; + + var agentIds = found.Select(x => x.AgentId).Distinct().ToList(); + var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); + var res = found.Select(x => new UserAgent { - AgentIds = agentIds - }; - var agents = GetAgents(filter); - return agents; + Id = x.Id, + UserId = x.UserId, + AgentId = x.AgentId, + Actions = x.Actions, + CreatedTime = x.CreatedTime, + UpdatedTime = x.UpdatedTime + }).ToList(); + + foreach (var item in res) + { + var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); + if (agent == null) continue; + + item.Agent = agent; + } + + return res; } public List GetAgentResponses(string agentId, string prefix, string intent) @@ -412,12 +429,15 @@ 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 = x.UserId, AgentId = x.AgentId, - UserId = !string.IsNullOrEmpty(x.UserId) ? x.UserId : string.Empty, - Editable = x.Editable, + Actions = x.Actions, CreatedTime = x.CreatedTime, UpdatedTime = x.UpdatedTime }).ToList(); @@ -446,11 +466,11 @@ public partial class MongoRepository if (string.IsNullOrEmpty(agentId)) return false; var agentFilter = Builders.Filter.Eq(x => x.Id, agentId); - var agentUserFilter = Builders.Filter.Eq(x => x.AgentId, agentId); + var userAgentFilter = Builders.Filter.Eq(x => x.AgentId, agentId); var agentTaskFilter = Builders.Filter.Eq(x => x.AgentId, agentId); _dc.Agents.DeleteOne(agentFilter); - _dc.UserAgents.DeleteMany(agentUserFilter); + _dc.UserAgents.DeleteMany(userAgentFilter); _dc.AgentTasks.DeleteMany(agentTaskFilter); return true; } 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.Transaction.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Transaction.cs deleted file mode 100644 index e2ffbb0e..00000000 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Transaction.cs +++ /dev/null @@ -1,157 +0,0 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Users.Models; - -namespace BotSharp.Plugin.MongoStorage.Repository; - -public partial class MongoRepository -{ - public void Add(object entity) - { - if (entity is Agent agent) - { - _agents.Add(agent); - _changedTableNames.Add(nameof(Agent)); - } - else if (entity is User user) - { - _users.Add(user); - _changedTableNames.Add(nameof(User)); - } - else if (entity is UserAgent userAgent) - { - _userAgents.Add(userAgent); - _changedTableNames.Add(nameof(UserAgent)); - } - } - - public int Transaction(Action action) - { - _changedTableNames.Clear(); - action(); - - foreach (var table in _changedTableNames) - { - if (table == nameof(Agent)) - { - var agents = _agents.Select(x => new AgentDocument - { - Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), - Name = x.Name, - IconUrl = x.IconUrl, - Description = x.Description, - Instruction = x.Instruction, - ChannelInstructions = x.ChannelInstructions? - .Select(i => ChannelInstructionMongoElement.ToMongoElement(i))? - .ToList() ?? new List(), - Templates = x.Templates? - .Select(t => AgentTemplateMongoElement.ToMongoElement(t))? - .ToList() ?? new List(), - Functions = x.Functions? - .Select(f => FunctionDefMongoElement.ToMongoElement(f))? - .ToList() ?? new List(), - Responses = x.Responses? - .Select(r => AgentResponseMongoElement.ToMongoElement(r))? - .ToList() ?? new List(), - Samples = x.Samples ?? new List(), - Utilities = x.Utilities ?? new List(), - IsPublic = x.IsPublic, - Type = x.Type, - InheritAgentId = x.InheritAgentId, - Disabled = x.Disabled, - Profiles = x.Profiles, - RoutingRules = x.RoutingRules? - .Select(r => RoutingRuleMongoElement.ToMongoElement(r))? - .ToList() ?? new List(), - LlmConfig = AgentLlmConfigMongoElement.ToMongoElement(x.LlmConfig), - CreatedTime = x.CreatedDateTime, - UpdatedTime = x.UpdatedDateTime - }).ToList(); - - foreach (var agent in agents) - { - var filter = Builders.Filter.Eq(x => x.Id, agent.Id); - var update = Builders.Update - .Set(x => x.Name, agent.Name) - .Set(x => x.Description, agent.Description) - .Set(x => x.Instruction, agent.Instruction) - .Set(x => x.ChannelInstructions, agent.ChannelInstructions) - .Set(x => x.Templates, agent.Templates) - .Set(x => x.Functions, agent.Functions) - .Set(x => x.Responses, agent.Responses) - .Set(x => x.Samples, agent.Samples) - .Set(x => x.Utilities, agent.Utilities) - .Set(x => x.IsPublic, agent.IsPublic) - .Set(x => x.Type, agent.Type) - .Set(x => x.InheritAgentId, agent.InheritAgentId) - .Set(x => x.Disabled, agent.Disabled) - .Set(x => x.Profiles, agent.Profiles) - .Set(x => x.RoutingRules, agent.RoutingRules) - .Set(x => x.LlmConfig, agent.LlmConfig) - .Set(x => x.CreatedTime, agent.CreatedTime) - .Set(x => x.UpdatedTime, agent.UpdatedTime); - _dc.Agents.UpdateOne(filter, update, _options); - } - } - else if (table == nameof(User)) - { - var users = _users.Select(x => new UserDocument - { - Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), - UserName = x.UserName, - FirstName = x.FirstName, - LastName = x.LastName, - Salt = x.Salt, - Password = x.Password, - Email = x.Email, - ExternalId = x.ExternalId, - Role = x.Role, - CreatedTime = x.CreatedTime, - UpdatedTime = x.UpdatedTime - }).ToList(); - - foreach (var user in users) - { - var filter = Builders.Filter.Eq(x => x.Id, user.Id); - var update = Builders.Update - .Set(x => x.UserName, user.UserName) - .Set(x => x.FirstName, user.FirstName) - .Set(x => x.LastName, user.LastName) - .Set(x => x.Email, user.Email) - .Set(x => x.Salt, user.Salt) - .Set(x => x.Password, user.Password) - .Set(x => x.ExternalId, user.ExternalId) - .Set(x => x.Role, user.Role) - .Set(x => x.CreatedTime, user.CreatedTime) - .Set(x => x.UpdatedTime, user.UpdatedTime); - _dc.Users.UpdateOne(filter, update, _options); - } - } - else if (table == nameof(UserAgent)) - { - var userAgents = _userAgents.Select(x => new UserAgentDocument - { - Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), - AgentId = x.AgentId, - UserId = !string.IsNullOrEmpty(x.UserId) ? x.UserId : string.Empty, - Editable = x.Editable, - CreatedTime = x.CreatedTime, - UpdatedTime = x.UpdatedTime - }).ToList(); - - foreach (var userAgent in userAgents) - { - var filter = Builders.Filter.Eq(x => x.Id, userAgent.Id); - var update = Builders.Update - .Set(x => x.AgentId, userAgent.AgentId) - .Set(x => x.UserId, userAgent.UserId) - .Set(x => x.Editable, userAgent.Editable) - .Set(x => x.CreatedTime, userAgent.CreatedTime) - .Set(x => x.UpdatedTime, userAgent.UpdatedTime); - _dc.UserAgents.UpdateOne(filter, update, _options); - } - } - } - - return _changedTableNames.Count; - } -} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs index f5dbc508..19d933a9 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.User.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Repositories.Filters; using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; @@ -168,4 +170,134 @@ public partial class MongoRepository UpdateUserIsDisable(userId, isDisable); } } + + public PagedItems GetUsers(UserFilter filter) + { + var userBuilder = Builders.Filter; + var userFilters = new List>() { userBuilder.Empty }; + + // Apply filters + if (!filter.UserIds.IsNullOrEmpty()) + { + userFilters.Add(userBuilder.In(x => x.Id, filter.UserIds)); + } + if (!filter.UserNames.IsNullOrEmpty()) + { + userFilters.Add(userBuilder.In(x => x.UserName, filter.UserNames)); + } + if (!filter.ExternalIds.IsNullOrEmpty()) + { + userFilters.Add(userBuilder.In(x => x.ExternalId, filter.ExternalIds)); + } + if (!filter.Roles.IsNullOrEmpty()) + { + userFilters.Add(userBuilder.In(x => x.Role, filter.Roles)); + } + if (!filter.Sources.IsNullOrEmpty()) + { + userFilters.Add(userBuilder.In(x => x.Source, filter.Sources)); + } + + // Filter def and sort + var filterDef = userBuilder.And(userFilters); + var sortDef = Builders.Sort.Descending(x => x.CreatedTime); + + // Search + var userDocs = _dc.Users.Find(filterDef).Sort(sortDef).Skip(filter.Offset).Limit(filter.Size).ToList(); + var count = _dc.Users.CountDocuments(filterDef); + + var users = userDocs.Select(x => x.ToUser()).ToList(); + var userIds = users.Select(x => x.Id).ToList(); + var userAgents = _dc.UserAgents.AsQueryable().Where(x => userIds.Contains(x.UserId)).Select(x => new UserAgent + { + Id = x.Id, + UserId = x.UserId, + AgentId = x.AgentId, + Actions = x.Actions ?? Enumerable.Empty(), + CreatedTime = x.CreatedTime, + UpdatedTime = x.UpdatedTime + }).ToList(); + var agentIds = userAgents.Select(x => x.AgentId).Distinct().ToList(); + + if (!agentIds.IsNullOrEmpty()) + { + var agents = GetAgents(new AgentFilter { AgentIds = agentIds }); + foreach (var item in userAgents) + { + var agent = agents.FirstOrDefault(x => x.Id == item.AgentId); + if (agent == null) continue; + + item.Agent = agent; + } + + foreach (var user in users) + { + var found = userAgents.Where(x => x.UserId == user.Id).ToList(); + if (found.IsNullOrEmpty()) continue; + + user.AgentActions = found.Select(x => new UserAgentAction + { + Id = x.Id, + AgentId = x.AgentId, + Agent = x.Agent, + Actions = x.Actions + }); + } + } + + return new PagedItems + { + Items = users, + Count = (int)count + }; + } + + + public bool UpdateUser(User user, bool isUpdateUserAgents = false) + { + if (string.IsNullOrEmpty(user?.Id)) return false; + + var userFilter = Builders.Filter.Eq(x => x.Id, user.Id); + var userUpdate = Builders.Update + .Set(x => x.Type, user.Type) + .Set(x => x.Role, user.Role) + .Set(x => x.Permissions, user.Permissions) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.Users.UpdateOne(userFilter, userUpdate); + + if (isUpdateUserAgents) + { + var userAgentDocs = user.AgentActions?.Select(x => new UserAgentDocument + { + Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(), + UserId = user.Id, + AgentId = x.AgentId, + Actions = x.Actions, + CreatedTime = DateTime.UtcNow, + UpdatedTime = DateTime.UtcNow + })?.ToList() ?? []; + + var toDelete = _dc.UserAgents.Find(Builders.Filter.And( + Builders.Filter.Eq(x => x.UserId, user.Id), + Builders.Filter.Nin(x => x.Id, userAgentDocs.Select(x => x.Id)) + )).ToList(); + + _dc.UserAgents.DeleteMany(Builders.Filter.In(x => x.Id, toDelete.Select(x => x.Id))); + foreach (var doc in userAgentDocs) + { + var userAgentFilter = Builders.Filter.Eq(x => x.Id, doc.Id); + var userAgentUpdate = Builders.Update + .Set(x => x.Id, doc.Id) + .Set(x => x.UserId, user.Id) + .Set(x => x.AgentId, doc.AgentId) + .Set(x => x.Actions, doc.Actions) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.UserAgents.UpdateOne(userAgentFilter, userAgentUpdate, _options); + } + } + + return true; + } } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs index 133edba1..258c1883 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.cs @@ -1,6 +1,3 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Conversations.Models; -using BotSharp.Abstraction.Users.Models; using Microsoft.Extensions.Logging; namespace BotSharp.Plugin.MongoStorage.Repository; @@ -26,9 +23,5 @@ public partial class MongoRepository : IBotSharpRepository }; } - private List _agents = new List(); - private List _users = new List(); - private List _userAgents = new List(); - private List _conversations = new List(); - List _changedTableNames = new List(); + 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/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",