diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs index db4db62a..b435a4d1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentService.cs @@ -48,5 +48,7 @@ public interface IAgentService string GetDataDir(); string GetAgentDataDir(string agentId); + List GetAgentsByUser(string userId); + PluginDef GetPlugin(string agentId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs index 055db3cc..c8b997ec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationService.cs @@ -54,4 +54,6 @@ public interface IConversationService /// /// Task UpdateBreakpoint(bool resetStates = false, string? reason = null, params string[] excludedStates); + + Task GetConversationSummary(IEnumerable conversationId); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs index 272abf0d..16197b9c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Files/IBotSharpFileService.cs @@ -5,8 +5,11 @@ public interface IBotSharpFileService string GetDirectory(string conversationId); IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2); IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false); - string? GetMessageFile(string conversationId, string messageId, string fileName); - void SaveMessageFiles(string conversationId, string messageId, List files); + string GetMessageFile(string conversationId, string messageId, string fileName); + bool SaveMessageFiles(string conversationId, string messageId, List files); + + string GetUserAvatar(); + bool SaveUserAvatar(BotSharpFile file); /// /// Delete files under messages diff --git a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs index bd5a1d88..5f8a59b9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Plugins/Models/PluginMenuDef.cs @@ -19,6 +19,9 @@ public class PluginMenuDef [JsonIgnore] public int Weight { get; set; } + [JsonIgnore] + public List? Roles { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? SubMenu { get; set; } diff --git a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs index eb21fb31..83ce823c 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/AgentPlugin.cs @@ -1,6 +1,7 @@ using BotSharp.Abstraction.MLTasks; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Settings; +using BotSharp.Abstraction.Users.Enums; using Microsoft.Extensions.Configuration; namespace BotSharp.Core.Agents; @@ -43,8 +44,8 @@ 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"), // icon: "bx bx-task" + new PluginMenuDef("Routing", link: "page/agent/router") { Roles = new List { UserRole.Admin } }, // icon: "bx bx-map-pin" + new PluginMenuDef("Evaluating", link: "page/agent/evaluator") { Roles = new List { 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 ce6512af..ae429484 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.CreateAgent.cs @@ -1,8 +1,4 @@ -using BotSharp.Abstraction.Agents.Models; -using BotSharp.Abstraction.Functions.Models; -using BotSharp.Abstraction.Repositories; using BotSharp.Abstraction.Tasks.Models; -using BotSharp.Abstraction.Users.Models; using System.IO; using System.Text.RegularExpressions; @@ -26,32 +22,13 @@ public partial class AgentService var dbSettings = _services.GetRequiredService(); var agentSettings = _services.GetRequiredService(); - var filePath = Path.Combine(dbSettings.FileRepository, agentSettings.DataDir); - var foundAgent = FetchAgentFileByName(agent.Name, filePath); - - if (foundAgent != null) - { - agentRecord.SetId(foundAgent.Id) - .SetName(foundAgent.Name) - .SetDescription(foundAgent.Description) - .SetIsPublic(foundAgent.IsPublic) - .SetDisabled(foundAgent.Disabled) - .SetAgentType(foundAgent.Type) - .SetProfiles(foundAgent.Profiles) - .SetRoutingRules(foundAgent.RoutingRules) - .SetInstruction(foundAgent.Instruction) - .SetTemplates(foundAgent.Templates) - .SetFunctions(foundAgent.Functions) - .SetResponses(foundAgent.Responses) - .SetLlmConfig(foundAgent.LlmConfig); - } var user = _db.GetUserById(_user.Id); var userAgentRecord = new UserAgent { Id = Guid.NewGuid().ToString(), UserId = user.Id, - AgentId = foundAgent?.Id ?? agentRecord.Id, + AgentId = agentRecord.Id, Editable = false, CreatedTime = DateTime.UtcNow, UpdatedTime = DateTime.UtcNow @@ -65,7 +42,7 @@ public partial class AgentService Utilities.ClearCache(); - return agentRecord; + return await Task.FromResult(agentRecord); } private Agent FetchAgentFileByName(string agentName, string filePath) diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs index 23111fd2..1fe5c6e1 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.DeleteAgent.cs @@ -1,9 +1,20 @@ +using BotSharp.Abstraction.Users.Enums; + namespace BotSharp.Core.Agents.Services; public partial class AgentService { public async Task DeleteAgent(string id) { - throw new NotImplementedException(); + var user = _db.GetUserById(_user.Id); + var agent = _db.GetAgentsByUser(_user.Id).FirstOrDefault(x => x.Id.IsEqualTo(id)); + + if (user?.Role != UserRole.Admin && agent == null) + { + return false; + } + + var deleted = _db.DeleteAgent(id); + return await Task.FromResult(deleted); } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 154d7398..af6cb65e 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Repositories.Enums; using BotSharp.Abstraction.Routing.Models; +using BotSharp.Abstraction.Users.Enums; using System.IO; namespace BotSharp.Core.Agents.Services; @@ -8,6 +9,10 @@ public partial class AgentService { public async Task UpdateAgent(Agent agent, AgentField updateField) { + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user?.Role != UserRole.Admin) return; + if (agent == null || string.IsNullOrEmpty(agent.Id)) return; var record = _db.GetAgent(agent.Id); diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs index bd009db0..da1b8cf1 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.cs @@ -47,4 +47,10 @@ public partial class AgentService : IAgentService } return dir; } + + public List GetAgentsByUser(string userId) + { + var agents = _db.GetAgentsByUser(userId); + return agents; + } } diff --git a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj index 0ddc52e8..10e21b3c 100644 --- a/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj +++ b/src/Infrastructure/BotSharp.Core/BotSharp.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -56,6 +56,7 @@ + @@ -142,6 +143,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs new file mode 100644 index 00000000..533020f0 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.Summary.cs @@ -0,0 +1,108 @@ +using BotSharp.Abstraction.MLTasks; +using BotSharp.Abstraction.Templating; + +namespace BotSharp.Core.Conversations.Services; + +public partial class ConversationService +{ + public async Task GetConversationSummary(IEnumerable conversationIds) + { + if (conversationIds.IsNullOrEmpty()) return string.Empty; + + var routing = _services.GetRequiredService(); + var agentService = _services.GetRequiredService(); + + var contents = new List(); + foreach ( var conversationId in conversationIds) + { + if (string.IsNullOrEmpty(conversationId)) continue; + + var dialogs = _storage.GetDialogs(conversationId); + + if (dialogs.IsNullOrEmpty()) continue; + + var content = GetConversationContent(dialogs); + contents.Add(content); + } + + var router = await agentService.LoadAgent(AIAssistant); + var prompt = GetPrompt(router, contents); + var summary = await Summarize(router, prompt); + + return summary; + } + + private string GetPrompt(Agent agent, List contents) + { + var template = agent.Templates.First(x => x.Name == "conversation.summary").Content; + var render = _services.GetRequiredService(); + + var texts = string.Empty; + for (int i = 0; i < contents.Count; i++) + { + texts += $"[Conversation {i+1}]\r\n{contents[i]}"; + } + + return render.Render(template, new Dictionary + { + { "texts", texts } + }); + } + + private async Task Summarize(Agent agent, string prompt) + { + var provider = "openai"; + string? model; + + var providerService = _services.GetRequiredService(); + var modelSettings = providerService.GetProviderModels(provider); + var modelSetting = modelSettings.FirstOrDefault(x => x.Name.IsEqualTo("gpt4-turbo") || x.Name.IsEqualTo("gpt-4o")); + + if (modelSetting != null) + { + model = modelSetting.Name; + } + else + { + provider = agent?.LlmConfig?.Provider; + model = agent?.LlmConfig?.Model; + if (provider == null || model == null) + { + var agentSettings = _services.GetRequiredService(); + provider = agentSettings.LlmConfig.Provider; + model = agentSettings.LlmConfig.Model; + } + } + + var chatCompletion = CompletionProvider.GetChatCompletion(_services, provider, model); + var response = await chatCompletion.GetChatCompletions(new Agent + { + Id = agent.Id, + Name = agent.Name, + Instruction = prompt + }, new List + { + new RoleDialogModel(AgentRole.User, "Please summarize the conversations.") + }); + + return response.Content; + } + + private string GetConversationContent(List dialogs, int maxDialogCount = 50) + { + var conversation = ""; + + foreach (var dialog in dialogs.TakeLast(maxDialogCount)) + { + var role = dialog.Role; + if (role != AgentRole.User) + { + role = AgentRole.Assistant; + } + + conversation += $"{role}: {dialog.Payload ?? dialog.Content}\r\n"; + } + + return conversation + "\r\n"; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs index 4de36b9d..8e113226 100644 --- a/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs +++ b/src/Infrastructure/BotSharp.Core/Conversations/Services/ConversationService.cs @@ -12,6 +12,8 @@ public partial class ConversationService : IConversationService private readonly IConversationStorage _storage; private readonly IConversationStateService _state; private string _conversationId; + private const string AIAssistant = "01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a"; + public string ConversationId => _conversationId; public IConversationStateService States => _state; diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs new file mode 100644 index 00000000..f12ecb60 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.Conversation.cs @@ -0,0 +1,188 @@ +using Microsoft.AspNetCore.StaticFiles; +using System.IO; +using System.Threading; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService +{ + public IEnumerable GetChatImages(string conversationId, List conversations, int offset = 1) + { + var files = new List(); + if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty()) + { + return files; + } + + if (offset <= 0) + { + offset = MIN_OFFSET; + } + else if (offset > MAX_OFFSET) + { + offset = MAX_OFFSET; + } + + var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList(); + files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList(); + return files; + } + + public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false) + { + var files = new List(); + if (messageIds.IsNullOrEmpty()) return files; + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) + { + continue; + } + + foreach (var file in Directory.GetFiles(dir)) + { + var contentType = GetFileContentType(file); + if (imageOnly && !_allowedTypes.Contains(contentType)) + { + continue; + } + + var fileName = Path.GetFileNameWithoutExtension(file); + var extension = Path.GetExtension(file); + var fileType = extension.Substring(1); + + var model = new MessageFileModel() + { + MessageId = messageId, + FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", + FileStorageUrl = file, + FileName = fileName, + FileType = fileType, + ContentType = contentType + }; + files.Add(model); + } + } + + return files; + } + + public string GetMessageFile(string conversationId, string messageId, string fileName) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (!ExistDirectory(dir)) + { + return string.Empty; + } + + var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); + return found; + } + + public bool SaveMessageFiles(string conversationId, string messageId, List files) + { + if (files.IsNullOrEmpty()) return false; + + var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); + if (!ExistDirectory(dir)) return false; + + try + { + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + if (string.IsNullOrEmpty(file.FileData)) + { + continue; + } + + var (_, bytes) = GetFileInfoFromData(file.FileData); + var fileType = Path.GetExtension(file.FileName); + var fileName = $"{i + 1}{fileType}"; + Thread.Sleep(100); + File.WriteAllBytes(Path.Combine(dir, fileName), bytes); + } + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving conversation files: {ex.Message}"); + return false; + } + } + + + + public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null) + { + if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false; + + if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId)) + { + var prevDir = GetConversationFileDirectory(conversationId, targetMessageId); + var newDir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, newMessageId); + + if (ExistDirectory(prevDir)) + { + if (ExistDirectory(newDir)) + { + Directory.Delete(newDir, true); + } + + Directory.Move(prevDir, newDir); + } + } + + foreach (var messageId in messageIds) + { + var dir = GetConversationFileDirectory(conversationId, messageId); + if (string.IsNullOrEmpty(dir)) continue; + + Thread.Sleep(100); + Directory.Delete(dir, true); + } + + return true; + } + + public bool DeleteConversationFiles(IEnumerable conversationIds) + { + if (conversationIds.IsNullOrEmpty()) return false; + + foreach (var conversationId in conversationIds) + { + var convDir = FindConversationDirectory(conversationId); + if (!ExistDirectory(convDir)) continue; + + Directory.Delete(convDir, true); + } + return true; + } + + #region Private methods + private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false) + { + if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) + { + return string.Empty; + } + + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); + if (!Directory.Exists(dir) && createNewDir) + { + Directory.CreateDirectory(dir); + } + return dir; + } + + private string? FindConversationDirectory(string conversationId) + { + if (string.IsNullOrEmpty(conversationId)) return null; + + var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId); + return dir; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs new file mode 100644 index 00000000..b6a87993 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.User.cs @@ -0,0 +1,65 @@ +using System.IO; + +namespace BotSharp.Core.Files; + +public partial class BotSharpFileService +{ + public string GetUserAvatar() + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var dir = GetUserAvatarDir(user?.Id); + + if (!ExistDirectory(dir)) return string.Empty; + + var found = Directory.GetFiles(dir).FirstOrDefault() ?? string.Empty; + return found; + } + + public bool SaveUserAvatar(BotSharpFile file) + { + if (file == null || string.IsNullOrEmpty(file.FileData)) return false; + + try + { + var db = _services.GetRequiredService(); + var user = db.GetUserById(_user.Id); + var dir = GetUserAvatarDir(user?.Id); + + if (string.IsNullOrEmpty(dir)) return false; + + if (Directory.Exists(dir)) + { + Directory.Delete(dir, true); + } + + dir = GetUserAvatarDir(user?.Id, createNewDir: true); + var (_, bytes) = GetFileInfoFromData(file.FileData); + File.WriteAllBytes(Path.Combine(dir, file.FileName), bytes); + return true; + } + catch (Exception ex) + { + _logger.LogWarning($"Error when saving user avatar: {ex.Message}"); + return false; + } + } + + + #region Private methods + private string GetUserAvatarDir(string? userId, bool createNewDir = false) + { + if (string.IsNullOrEmpty(userId)) + { + return string.Empty; + } + + var dir = Path.Combine(_baseDir, USERS_FOLDER, userId, USER_AVATAR_FOLDER); + if (!Directory.Exists(dir) && createNewDir) + { + Directory.CreateDirectory(dir); + } + return dir; + } + #endregion +} diff --git a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs index d7e961be..76d26dbc 100644 --- a/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs +++ b/src/Infrastructure/BotSharp.Core/Files/BotSharpFileService.cs @@ -1,28 +1,35 @@ using Microsoft.AspNetCore.StaticFiles; +using System; using System.IO; using System.Threading; namespace BotSharp.Core.Files; -public class BotSharpFileService : IBotSharpFileService +public partial class BotSharpFileService : IBotSharpFileService { private readonly BotSharpDatabaseSettings _dbSettings; private readonly IServiceProvider _services; + private readonly IUserIdentity _user; private readonly ILogger _logger; private readonly string _baseDir; private readonly IEnumerable _allowedTypes = new List { "image/png", "image/jpeg" }; private const string CONVERSATION_FOLDER = "conversations"; private const string FILE_FOLDER = "files"; + private const string USERS_FOLDER = "users"; + private const string USER_AVATAR_FOLDER = "avatar"; + private const int MIN_OFFSET = 1; private const int MAX_OFFSET = 5; public BotSharpFileService( BotSharpDatabaseSettings dbSettings, + IUserIdentity user, ILogger logger, IServiceProvider services) { _dbSettings = dbSettings; + _user = user; _logger = logger; _services = services; _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbSettings.FileRepository); @@ -38,157 +45,6 @@ public class BotSharpFileService : IBotSharpFileService return dir; } - public IEnumerable GetChatImages(string conversationId, List conversations, int offset = 2) - { - var files = new List(); - if (string.IsNullOrEmpty(conversationId) || conversations.IsNullOrEmpty()) - { - return files; - } - - if (offset <= 0) - { - offset = MIN_OFFSET; - } - else if (offset > MAX_OFFSET) - { - offset = MAX_OFFSET; - } - - var messageIds = conversations.Select(x => x.MessageId).Distinct().TakeLast(offset).ToList(); - files = GetMessageFiles(conversationId, messageIds, imageOnly: true).ToList(); - return files; - } - - public IEnumerable GetMessageFiles(string conversationId, IEnumerable messageIds, bool imageOnly = false) - { - var files = new List(); - if (messageIds.IsNullOrEmpty()) return files; - - foreach (var messageId in messageIds) - { - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) - { - continue; - } - - foreach (var file in Directory.GetFiles(dir)) - { - var contentType = GetFileContentType(file); - if (imageOnly && !_allowedTypes.Contains(contentType)) - { - continue; - } - - var fileName = Path.GetFileNameWithoutExtension(file); - var extension = Path.GetExtension(file); - var fileType = extension.Substring(1); - - var model = new MessageFileModel() - { - MessageId = messageId, - FileUrl = $"/conversation/{conversationId}/message/{messageId}/file/{fileName}", - FileStorageUrl = file, - FileName = fileName, - FileType = fileType, - ContentType = contentType - }; - files.Add(model); - } - } - - return files; - } - - public string? GetMessageFile(string conversationId, string messageId, string fileName) - { - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) - { - return null; - } - - var found = Directory.GetFiles(dir).FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).IsEqualTo(fileName)); - return found; - } - - public void SaveMessageFiles(string conversationId, string messageId, List files) - { - if (files.IsNullOrEmpty()) return; - - var dir = GetConversationFileDirectory(conversationId, messageId, createNewDir: true); - if (string.IsNullOrEmpty(dir)) return; - - try - { - for (int i = 0; i < files.Count; i++) - { - var file = files[i]; - if (string.IsNullOrEmpty(file.FileData)) - { - continue; - } - - var (_, bytes) = GetFileInfoFromData(file.FileData); - var fileType = Path.GetExtension(file.FileName); - var fileName = $"{i + 1}{fileType}"; - Thread.Sleep(100); - File.WriteAllBytes(Path.Combine(dir, fileName), bytes); - } - } - catch (Exception ex) - { - _logger.LogError($"Error when saving conversation files: {ex.Message}"); - } - } - - public bool DeleteMessageFiles(string conversationId, IEnumerable messageIds, string targetMessageId, string? newMessageId = null) - { - if (string.IsNullOrEmpty(conversationId) || messageIds == null) return false; - - if (!string.IsNullOrEmpty(targetMessageId) && !string.IsNullOrEmpty(newMessageId)) - { - var prevDir = GetConversationFileDirectory(conversationId, targetMessageId); - var newDir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, newMessageId); - - if (Directory.Exists(prevDir)) - { - if (Directory.Exists(newDir)) - { - Directory.Delete(newDir, true); - } - - Directory.Move(prevDir, newDir); - } - } - - foreach ( var messageId in messageIds) - { - var dir = GetConversationFileDirectory(conversationId, messageId); - if (string.IsNullOrEmpty(dir)) continue; - - Thread.Sleep(100); - Directory.Delete(dir, true); - } - - return true; - } - - public bool DeleteConversationFiles(IEnumerable conversationIds) - { - if (conversationIds.IsNullOrEmpty()) return false; - - foreach (var conversationId in conversationIds) - { - var convDir = FindConversationDirectory(conversationId); - if (string.IsNullOrEmpty(convDir)) continue; - - Directory.Delete(convDir, true); - } - return true; - } - public (string, byte[]) GetFileInfoFromData(string data) { if (string.IsNullOrEmpty(data)) @@ -207,38 +63,6 @@ public class BotSharpFileService : IBotSharpFileService } #region Private methods - private string GetConversationFileDirectory(string? conversationId, string? messageId, bool createNewDir = false) - { - if (string.IsNullOrEmpty(conversationId) || string.IsNullOrEmpty(messageId)) - { - return string.Empty; - } - - var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId, FILE_FOLDER, messageId); - if (!Directory.Exists(dir)) - { - if (createNewDir) - { - Directory.CreateDirectory(dir); - } - else - { - return string.Empty; - } - } - return dir; - } - - private string? FindConversationDirectory(string conversationId) - { - if (string.IsNullOrEmpty(conversationId)) return null; - - var dir = Path.Combine(_baseDir, CONVERSATION_FOLDER, conversationId); - if (!Directory.Exists(dir)) return null; - - return dir; - } - private string GetFileContentType(string filePath) { string contentType; @@ -250,5 +74,10 @@ public class BotSharpFileService : IBotSharpFileService return contentType; } + + private bool ExistDirectory(string? dir) + { + return !string.IsNullOrEmpty(dir) && Directory.Exists(dir); + } #endregion } diff --git a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs index f6de19ac..1b883657 100644 --- a/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs +++ b/src/Infrastructure/BotSharp.Core/Plugins/PluginLoader.cs @@ -269,4 +269,20 @@ public class PluginLoader } }); } + + public List GetPluginMenuByRoles(List plugins, string userRole) + { + if (plugins.IsNullOrEmpty()) return plugins; + + var filtered = new List(); + foreach (var plugin in plugins) + { + if (plugin.Roles.IsNullOrEmpty() || plugin.Roles.Contains(userRole)) + { + plugin.SubMenu = GetPluginMenuByRoles(plugin.SubMenu, userRole); + filtered.Add(plugin); + } + } + return filtered; + } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index a46669a5..b7141a5b 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -436,7 +436,40 @@ namespace BotSharp.Core.Repository public bool DeleteAgent(string agentId) { - return false; + if (string.IsNullOrEmpty(agentId)) return false; + + try + { + var agentDir = GetAgentDataDir(agentId); + if (string.IsNullOrEmpty(agentDir)) return false; + + // Delete agent user relationships + var usersDir = Path.Combine(_dbSettings.FileRepository, "users"); + if (Directory.Exists(usersDir)) + { + foreach (var userDir in Directory.GetDirectories(usersDir)) + { + var userAgentFile = Directory.GetFiles(userDir).FirstOrDefault(x => Path.GetFileName(x) == USER_AGENT_FILE); + if (string.IsNullOrEmpty(userAgentFile)) continue; + + var text = File.ReadAllText(userAgentFile); + var userAgents = JsonSerializer.Deserialize>(text, _options); + if (userAgents.IsNullOrEmpty()) continue; + + userAgents = userAgents.Where(x => x.AgentId != agentId).ToList(); + File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options)); + } + } + + // Delete agent folder + Directory.Delete(agentDir, true); + + return true; + } + catch + { + return false; + } } } } diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs index 36f17b85..e7566002 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.User.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Users.Enums; using BotSharp.Abstraction.Users.Models; using System.IO; diff --git a/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs b/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs index ddf709ec..27c55ab5 100644 --- a/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs +++ b/src/Infrastructure/BotSharp.Core/Tasks/TaskPlugin.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Plugins.Models; using BotSharp.Abstraction.Tasks; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Core.Tasks.Services; using Microsoft.Extensions.Configuration; @@ -19,7 +20,10 @@ public class TaskPlugin : IBotSharpPlugin public bool AttachMenu(List menu) { var section = menu.First(x => x.Label == "Apps"); - menu.Add(new PluginMenuDef("Task", link: "page/task", icon: "bx bx-task", weight: section.Weight + 8)); + menu.Add(new PluginMenuDef("Task", link: "page/task", icon: "bx bx-task", weight: section.Weight + 8) + { + Roles = new List { UserRole.Admin } + }); return true; } diff --git a/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/conversation.summary.liquid b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/conversation.summary.liquid new file mode 100644 index 00000000..bb4e764f --- /dev/null +++ b/src/Infrastructure/BotSharp.Core/data/agents/01fcc3e5-9af7-49e6-ad7a-a760bd12dc4a/templates/conversation.summary.liquid @@ -0,0 +1,14 @@ +Please read each conversation in the [CONVERSATIONS] section and provide a summary. + +*** Super Important! Please consider every conversation. Do not only consider the recent sentences. *** +** Please do not respond to the latest conversation. +** If there are different topics in the conversations, please summarize each topic in different sentences and list them in bullets. +* Please use concise sentences to summarize each topic. +* Please do not include excessive details in the summaries. +* Please use 'user' instead of 'you', 'he' or 'she'. + +[CONVERSATIONS] + +{% for text in texts -%} +{{ text }}{{ "\r\n" }} +{%- endfor %} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs index bb67de2c..a7b5652d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/AgentController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Users.Enums; namespace BotSharp.OpenAPI.Controllers; @@ -7,11 +8,13 @@ namespace BotSharp.OpenAPI.Controllers; public class AgentController : ControllerBase { private readonly IAgentService _agentService; + private readonly IUserIdentity _user; private readonly IServiceProvider _services; - public AgentController(IAgentService agentService, IServiceProvider services) + public AgentController(IAgentService agentService, IUserIdentity user, IServiceProvider services) { _agentService = agentService; + _user = user; _services = services; } @@ -23,7 +26,7 @@ public class AgentController : ControllerBase } [HttpGet("/agent/{id}")] - public async Task GetAgent([FromRoute] string id) + public async Task GetAgent([FromRoute] string id) { var agents = await GetAgents(new AgentFilter { @@ -31,6 +34,8 @@ public class AgentController : ControllerBase }); var targetAgent = agents.Items.FirstOrDefault(); + if (targetAgent == null) return null; + var redirectAgentIds = targetAgent.RoutingRules .Where(x => !string.IsNullOrEmpty(x.RedirectTo)) .Select(x => x.RedirectTo).ToList(); @@ -45,6 +50,17 @@ public class AgentController : ControllerBase rule.RedirectToAgentName = found.Name; } + + var editable = true; + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user?.Role != UserRole.Admin) + { + var userAgents = _agentService.GetAgentsByUser(user?.Id); + editable = userAgents?.Select(x => x.Id)?.Contains(targetAgent.Id) ?? false; + } + + targetAgent.Editable = editable; return targetAgent; } @@ -118,4 +134,10 @@ public class AgentController : ControllerBase model.Id = agentId; return await _agentService.PatchAgentTemplate(model); } + + [HttpDelete("/agent/{agentId}")] + public async Task DeleteAgent([FromRoute] string agentId) + { + return await _agentService.DeleteAgent(agentId); + } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs index 71d824b3..adc37636 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/ConversationController.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Options; using BotSharp.Abstraction.Routing; +using BotSharp.Abstraction.Users.Enums; namespace BotSharp.OpenAPI.Controllers; @@ -41,20 +42,23 @@ public class ConversationController : ControllerBase [HttpPost("/conversations")] public async Task> GetConversations([FromBody] ConversationFilter filter) { - var service = _services.GetRequiredService(); - var conversations = await service.GetConversations(filter); - + var convService = _services.GetRequiredService(); var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user == null) + { + return new PagedItems(); + } + + filter.UserId = user.Role != UserRole.Admin ? user.Id : null; + var conversations = await convService.GetConversations(filter); var agentService = _services.GetRequiredService(); - var list = conversations.Items - .Select(x => ConversationViewModel.FromSession(x)) - .ToList(); + var list = conversations.Items.Select(x => ConversationViewModel.FromSession(x)).ToList(); foreach (var item in list) { - var user = await userService.GetUser(item.User.Id); + user = await userService.GetUser(item.User.Id); item.User = UserViewModel.FromUser(user); - var agent = await agentService.GetAgent(item.AgentId); item.AgentName = agent?.Name; } @@ -119,26 +123,42 @@ public class ConversationController : ControllerBase } [HttpGet("/conversation/{conversationId}")] - public async Task GetConversation([FromRoute] string conversationId) + public async Task GetConversation([FromRoute] string conversationId) { var service = _services.GetRequiredService(); - var conversations = await service.GetConversations(new ConversationFilter - { - Id = conversationId - }); - var userService = _services.GetRequiredService(); - var result = ConversationViewModel.FromSession(conversations.Items.First()); + var user = await userService.GetUser(_user.Id); + if (user == null) + { + return null; + } + var filter = new ConversationFilter + { + Id = conversationId, + UserId = user.Role != UserRole.Admin ? user.Id : null + }; + var conversations = await service.GetConversations(filter); + if (conversations.Items.IsNullOrEmpty()) + { + return null; + } + + var result = ConversationViewModel.FromSession(conversations.Items.First()); var state = _services.GetRequiredService(); result.States = state.Load(conversationId, isReadOnly: true); - - var user = await userService.GetUser(result.User.Id); result.User = UserViewModel.FromUser(user); return result; } + [HttpPost("/conversation/summary")] + public async Task GetConversationSummary([FromBody] ConversationSummaryModel input) + { + var service = _services.GetRequiredService(); + return await service.GetConversationSummary(input.ConversationIds); + } + [HttpGet("/conversation/{conversationId}/user")] public async Task GetConversationUser([FromRoute] string conversationId) { @@ -171,7 +191,22 @@ public class ConversationController : ControllerBase [HttpDelete("/conversation/{conversationId}")] public async Task DeleteConversation([FromRoute] string conversationId) { + var userService = _services.GetRequiredService(); var conversationService = _services.GetRequiredService(); + + var user = await userService.GetUser(_user.Id); + var filter = new ConversationFilter + { + Id = conversationId, + UserId = user.Role != UserRole.Admin ? user.Id : null + }; + var conversations = await conversationService.GetConversations(filter); + + if (conversations.Items.IsNullOrEmpty()) + { + return false; + } + var response = await conversationService.DeleteConversations(new List { conversationId }); return response; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs index cfae602c..0c7fa255 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/FileController.cs @@ -46,7 +46,7 @@ public class FileController : ControllerBase } [HttpGet("/conversation/{conversationId}/message/{messageId}/file/{fileName}")] - public async Task GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string fileName) + public IActionResult GetMessageFile([FromRoute] string conversationId, [FromRoute] string messageId, [FromRoute] string fileName) { var fileService = _services.GetRequiredService(); var file = fileService.GetMessageFile(conversationId, messageId, fileName); @@ -54,7 +54,30 @@ public class FileController : ControllerBase { return NotFound(); } + return BuildFileResult(file); + } + [HttpPost("/user/avatar")] + public bool UploadUserAvatar([FromBody] BotSharpFile file) + { + var fileService = _services.GetRequiredService(); + return fileService.SaveUserAvatar(file); + } + + [HttpGet("/user/avatar")] + public IActionResult GetUserAvatar() + { + var fileService = _services.GetRequiredService(); + var file = fileService.GetUserAvatar(); + if (string.IsNullOrEmpty(file)) + { + return NotFound(); + } + return BuildFileResult(file); + } + + private FileContentResult BuildFileResult(string file) + { using Stream 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); diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs index 2c231e16..342f39fb 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/PluginController.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Plugins.Models; +using BotSharp.Abstraction.Users.Enums; using BotSharp.Core.Plugins; namespace BotSharp.OpenAPI.Controllers; @@ -8,23 +9,32 @@ namespace BotSharp.OpenAPI.Controllers; public class PluginController : ControllerBase { private readonly IServiceProvider _services; + private readonly IUserIdentity _user; private readonly PluginSettings _settings; - public PluginController(IServiceProvider services, PluginSettings settings) + public PluginController(IServiceProvider services, IUserIdentity user, PluginSettings settings) { _services = services; + _user = user; _settings = settings; } [HttpGet("/plugins")] - public PagedItems GetPlugins([FromQuery] PluginFilter filter) + public async Task> GetPlugins([FromQuery] PluginFilter filter) { + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + if (user?.Role != UserRole.Admin) + { + return new PagedItems(); + } + var loader = _services.GetRequiredService(); return loader.GetPagedPlugins(_services, filter); } [HttpGet("/plugin/menu")] - public List GetPluginMenu() + public async Task> GetPluginMenu() { var menu = new List { @@ -33,11 +43,18 @@ public class PluginController : ControllerBase IsHeader = true, }, new PluginMenuDef("System", weight: 30) - { - IsHeader = true + { + IsHeader = true, + Roles = new List { UserRole.Admin } }, - new PluginMenuDef("Plugins", link: "page/plugin", icon: "bx bx-plug", weight: 31), - new PluginMenuDef("Settings", link: "page/setting", icon: "bx bx-cog", weight: 32), + new PluginMenuDef("Plugins", link: "page/plugin", icon: "bx bx-plug", weight: 31) + { + Roles = new List { UserRole.Admin } + }, + new PluginMenuDef("Settings", link: "page/setting", icon: "bx bx-cog", weight: 32) + { + Roles = new List { UserRole.Admin } + } }; var loader = _services.GetRequiredService(); @@ -49,6 +66,10 @@ public class PluginController : ControllerBase } plugin.Module.AttachMenu(menu); } + + var userService = _services.GetRequiredService(); + var user = await userService.GetUser(_user.Id); + menu = loader.GetPluginMenuByRoles(menu, user?.Role); menu = menu.OrderBy(x => x.Weight).ToList(); return menu; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs index 536f5b5d..9d87a3e2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/AgentViewModel.cs @@ -42,6 +42,8 @@ public class AgentViewModel public PluginDef Plugin { get; set; } + public bool Editable { get; set; } + [JsonPropertyName("created_datetime")] public DateTime CreatedDateTime { get; set; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs new file mode 100644 index 00000000..0854ab2a --- /dev/null +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Conversations/ConversationSummaryModel.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.OpenAPI.ViewModels.Conversations; + +public class ConversationSummaryModel +{ + [JsonPropertyName("conversation_ids")] + public List ConversationIds { get; set; } = new List(); +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index eeb1a8a2..8d28cf03 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -19,6 +19,7 @@ public class UserViewModel public string Source { get; set; } [JsonPropertyName("external_id")] public string? ExternalId { get; set; } + public string Avatar { get; set; } = "/user/avatar"; [JsonPropertyName("create_date")] public DateTime CreateDate { get; set; } [JsonPropertyName("update_date")] @@ -47,7 +48,8 @@ public class UserViewModel Source = user.Source, ExternalId = user.ExternalId, CreateDate = user.CreatedTime, - UpdateDate = user.UpdatedTime + UpdateDate = user.UpdatedTime, + Avatar = "/user/avatar" }; } } diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs index e20f8602..79cb803a 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -14,13 +14,11 @@ public class WebSocketsMiddleware public async Task Invoke(HttpContext httpContext) { - var request = httpContext.Request;; - var messageFileRegex = new Regex(@"/conversation/[a-z0-9-]+/message/[a-z0-9-]+/file/[a-z0-9-]+", RegexOptions.IgnoreCase); + var request = httpContext.Request; // web sockets cannot pass headers so we must take the access token from query param and // add it to the header before authentication middleware runs - if ((request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) - || messageFileRegex.IsMatch(request.Path.Value ?? string.Empty)) && + if ((VerifyChatHubRequest(request) || VerifyGetRequest(request)) && request.Query.TryGetValue("access_token", out var accessToken)) { request.Headers["Authorization"] = $"Bearer {accessToken}"; @@ -28,4 +26,20 @@ public class WebSocketsMiddleware await _next(httpContext); } + + private bool VerifyChatHubRequest(HttpRequest request) + { + return request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase); + } + + private bool VerifyGetRequest(HttpRequest request) + { + var regexes = new List + { + new Regex(@"/conversation/[a-z0-9-]+/message/[a-z0-9-]+/file/[a-z0-9-]+", RegexOptions.IgnoreCase), + new Regex(@"/user/avatar", RegexOptions.IgnoreCase) + }; + + return request.Method.IsEqualTo("GET") && regexes.Any(x => x.IsMatch(request.Path.Value ?? string.Empty)); + } }