Merge branch 'master' into master

This commit is contained in:
Haiping 2024-11-18 19:06:56 +00:00 committed by GitHub
commit b7d4ccae54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1461 additions and 201 deletions

View file

@ -117,7 +117,9 @@ 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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Core.SideCar", "src\Infrastructure\BotSharp.Core.SideCar\BotSharp.Core.SideCar.csproj", "{6D3A54F9-4792-41DB-BE7D-4F7B1D918EAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.VertexAI", "src\Plugins\BotSharp.Plugin.LangChain\BotSharp.Plugin.VertexAI.csproj", "{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -479,6 +481,14 @@ Global
{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
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x64.ActiveCfg = Debug|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Debug|x64.Build.0 = Debug|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|Any CPU.Build.0 = Release|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.ActiveCfg = Release|Any CPU
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -536,6 +546,7 @@ Global
{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}
{7DA2DCD0-551B-432E-AA5C-22DDD3ED459B} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -30,7 +30,7 @@ public interface IKnowledgeService
/// <param name="collectionName"></param>
/// <param name="files"></param>
/// <returns></returns>
Task<UploadKnowledgeResponse> UploadDocumentsToKnowledge(string collectionName, IEnumerable<ExternalFileModel> files);
Task<UploadKnowledgeResponse> UploadDocumentsToKnowledge(string collectionName, IEnumerable<ExternalFileModel> files, ChunkOption? option = null);
/// <summary>
/// Save document content to knowledgebase without saving the document
/// </summary>

View file

@ -13,4 +13,15 @@ public class ChunkOption
public int Conjunction { get; set; }
public bool SplitByWord { get; set; }
public static ChunkOption Default()
{
return new ChunkOption
{
Size = 1024,
Conjunction = 12,
SplitByWord = true,
};
}
}

View file

@ -4,5 +4,6 @@ namespace BotSharp.Abstraction.Plugins.Models
{
public Pagination Pager { get; set; } = new Pagination();
public IEnumerable<string>? Names { get; set; }
public string? SimilarName { get; set; }
}
}

View file

@ -4,9 +4,15 @@ public class AgentFilter
{
public Pagination Pager { get; set; } = new Pagination();
public string? AgentName { get; set; }
public string? SimilarName { get; set; }
public bool? Disabled { get; set; }
public bool? Installed { get; set; }
public string? Type { get; set; }
public bool? IsPublic { get; set; }
public List<string>? AgentIds { get; set; }
public static AgentFilter Empty()
{
return new AgentFilter();
}
}

View file

@ -5,4 +5,9 @@ public class AgentTaskFilter
public Pagination Pager { get; set; } = new Pagination();
public string? AgentId { get; set; }
public bool? Enabled { get; set; }
public static AgentTaskFilter Empty()
{
return new AgentTaskFilter();
}
}

View file

@ -25,4 +25,9 @@ public class ConversationFilter
public IEnumerable<KeyValue>? States { get; set; } = [];
public IEnumerable<string>? Tags { get; set; } = [];
public static ConversationFilter Empty()
{
return new ConversationFilter();
}
}

View file

@ -0,0 +1,18 @@
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.Abstraction.Repositories.Filters;
public class RoleFilter
{
[JsonPropertyName("names")]
public IEnumerable<string>? Names { get; set; }
[JsonPropertyName("exclude_roles")]
public IEnumerable<string>? ExcludeRoles { get; set; } = UserConstant.AdminRoles;
public static RoleFilter Empty()
{
return new RoleFilter();
}
}

View file

@ -1,4 +1,4 @@
namespace BotSharp.Abstraction.Users.Models;
namespace BotSharp.Abstraction.Repositories.Filters;
public class UserFilter : Pagination
{
@ -14,6 +14,14 @@ public class UserFilter : Pagination
[JsonPropertyName("roles")]
public IEnumerable<string>? Roles { get; set; }
[JsonPropertyName("types")]
public IEnumerable<string>? Types { get; set; }
[JsonPropertyName("sources")]
public IEnumerable<string>? Sources { get; set; }
public static UserFilter Empty()
{
return new UserFilter();
}
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Roles.Models;
using BotSharp.Abstraction.Shared;
using BotSharp.Abstraction.Tasks.Models;
using BotSharp.Abstraction.Translation.Models;
@ -16,6 +17,13 @@ public interface IBotSharpRepository : IHaveServiceProvider
void SavePluginConfig(PluginConfig config);
#endregion
#region Role
bool RefreshRoles(IEnumerable<Role> roles) => throw new NotImplementedException();
IEnumerable<Role> GetRoles(RoleFilter filter) => throw new NotImplementedException();
Role? GetRoleDetails(string roleId, bool includeAgent = false) => throw new NotImplementedException();
bool UpdateRole(Role role, bool updateRoleAgents = false) => throw new NotImplementedException();
#endregion
#region User
User? GetUserByEmail(string email) => throw new NotImplementedException();
User? GetUserByPhone(string phone, string role = null, string regionCode = "CN") => throw new NotImplementedException();
@ -34,7 +42,8 @@ public interface IBotSharpRepository : IHaveServiceProvider
void UpdateUserIsDisable(string userId, bool isDisable) => throw new NotImplementedException();
void UpdateUsersIsDisable(List<string> userIds, bool isDisable) => throw new NotImplementedException();
PagedItems<User> GetUsers(UserFilter filter) => throw new NotImplementedException();
bool UpdateUser(User user, bool isUpdateUserAgents = false) => throw new NotImplementedException();
User? GetUserDetails(string userId, bool includeAgent = false) => throw new NotImplementedException();
bool UpdateUser(User user, bool updateUserAgents = false) => throw new NotImplementedException();
#endregion
#region Agent

View file

@ -0,0 +1,13 @@
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Roles.Models;
namespace BotSharp.Abstraction.Roles;
public interface IRoleService
{
Task<bool> RefreshRoles();
Task<IEnumerable<string>> GetRoleOptions();
Task<IEnumerable<Role>> GetRoles(RoleFilter filter);
Task<Role?> GetRoleDetails(string roleId, bool includeAgent = false);
Task<bool> UpdateRole(Role role, bool isUpdateRoleAgents = false);
}

View file

@ -0,0 +1,27 @@
namespace BotSharp.Abstraction.Roles.Models;
public class Role
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("permissions")]
public IEnumerable<string> Permissions { get; set; } = [];
[JsonIgnore]
public IEnumerable<RoleAgentAction> AgentActions { get; set; } = [];
[JsonPropertyName("updated_time")]
public DateTime UpdatedTime { get; set; }
[JsonPropertyName("created_time")]
public DateTime CreatedTime { get; set; }
public override string ToString()
{
return Name;
}
}

View file

@ -0,0 +1,25 @@
namespace BotSharp.Abstraction.Roles.Models;
public class RoleAgent
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("user_id")]
public string RoleId { get; set; } = string.Empty;
[JsonPropertyName("agent_id")]
public string AgentId { get; set; }
[JsonPropertyName("actions")]
public IEnumerable<string> Actions { get; set; } = [];
[JsonIgnore]
public Agent? Agent { get; set; }
[JsonPropertyName("updated_time")]
public DateTime UpdatedTime { get; set; }
[JsonPropertyName("created_time")]
public DateTime CreatedTime { get; set; }
}

View file

@ -0,0 +1,16 @@
namespace BotSharp.Abstraction.Roles.Models;
public class RoleAgentAction
{
[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<string> Actions { get; set; } = [];
}

View file

@ -4,4 +4,6 @@ public static class UserAction
{
public const string Edit = "edit";
public const string Chat = "chat";
public const string Train = "train";
public const string Evaluate = "evaluate";
}

View file

@ -2,21 +2,23 @@ namespace BotSharp.Abstraction.Users.Enums;
public class UserRole
{
public const string Root = "root";
/// <summary>
/// Admin account
/// </summary>
public const string Admin = "admin";
/// <summary>
/// Customer service representative (CSR)
/// </summary>
public const string CSR = "csr";
/// <summary>
/// Authorized user
/// </summary>
public const string User = "user";
/// <summary>
/// Customer service representative (CSR)
/// </summary>
public const string CSR = "csr";
/// <summary>
/// Back office operations
/// </summary>
@ -33,6 +35,4 @@ public class UserRole
/// AI Assistant
/// </summary>
public const string Assistant = "assistant";
public const string Root = "root";
}

View file

@ -1,3 +1,4 @@
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Users.Models;
using BotSharp.OpenAPI.ViewModels.Users;
@ -7,7 +8,10 @@ public interface IUserService
{
Task<User> GetUser(string id);
Task<PagedItems<User>> GetUsers(UserFilter filter);
Task<bool> UpdateUser(User model, bool isUpdateUserAgents = false);
Task<User?> GetUserDetails(string userId, bool includeAgent = false);
Task<bool> IsAdminUser(string userId);
Task<UserAuthorization> GetUserAuthorizations(IEnumerable<string>? agentIds = null);
Task<bool> UpdateUser(User user, bool isUpdateUserAgents = false);
Task<User> CreateUser(User user);
Task<Token> ActiveUser(UserActivationModel model);
Task<Token?> GetAffiliateToken(string authorization);

View file

@ -0,0 +1,25 @@
namespace BotSharp.Abstraction.Users.Models;
public class UserAuthorization
{
public bool IsAdmin { get; set; }
public IEnumerable<string> Permissions { get; set; } = [];
public IEnumerable<UserAgent> AgentActions { get; set; } = [];
}
public static class UserAuthorizationExtension
{
public static bool IsAgentActionAllowed(this UserAuthorization auth, string agentId, string targetAction)
{
if (auth == null || string.IsNullOrEmpty(agentId)) return false;
if (auth.IsAdmin) return true;
var found = auth.AgentActions.FirstOrDefault(x => x.AgentId == agentId);
if (found == null) return false;
var actions = found.Actions ?? [];
return actions.Any(x => x == targetAction);
}
}

View file

@ -8,7 +8,7 @@ namespace BotSharp.Core.SideCar;
public class BotSharpSideCarPlugin : IBotSharpPlugin
{
public string Id => "06e5a276-bba0-45af-9625-889267c341c9";
public string Name => "Side car";
public string Name => "Side Car";
public string Description => "Provides side car for calling agent cluster in conversation";
public SettingsMeta Settings => new SettingsMeta("SideCar");

View file

@ -25,8 +25,11 @@ public partial class AgentService
var agentSettings = _services.GetRequiredService<AgentSettings>();
var user = _db.GetUserById(_user.Id);
var userService = _services.GetRequiredService<IUserService>();
var auth = await userService.GetUserAuthorizations();
_db.BulkInsertAgents(new List<Agent> { agentRecord });
if (!UserConstant.AdminRoles.Contains(user.Role))
if (auth.IsAdmin || auth.Permissions.Contains(UserPermission.CreateAgent))
{
_db.BulkInsertUserAgents(new List<UserAgent>
{
@ -34,7 +37,7 @@ public partial class AgentService
{
UserId = user.Id,
AgentId = agentRecord.Id,
Actions = new List<string> { UserAction.Edit, UserAction.Chat },
Actions = new List<string> { UserAction.Edit, UserAction.Train, UserAction.Evaluate, UserAction.Chat },
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Models;
namespace BotSharp.Core.Agents.Services;
@ -6,11 +7,10 @@ public partial class AgentService
{
public async Task<bool> DeleteAgent(string id)
{
var user = _db.GetUserById(_user.Id);
var userAgents = await GetUserAgents(user?.Id);
var found = userAgents?.FirstOrDefault(x => x.AgentId == id);
var userService = _services.GetRequiredService<IUserService>();
var auth = await userService.GetUserAuthorizations(new List<string> { id });
if (!UserConstant.AdminRoles.Contains(user?.Role) && (found?.Actions == null || !found.Actions.Contains(UserAction.Edit)))
if (!auth.IsAgentActionAllowed(id, UserAction.Edit))
{
return false;
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Repositories.Enums;
using BotSharp.Abstraction.Users.Enums;
using System.IO;
namespace BotSharp.Core.Agents.Services;
@ -17,8 +16,9 @@ public partial class AgentService
return refreshResult;
}
var user = _db.GetUserById(_user.Id);
if (!UserConstant.AdminRoles.Contains(user.Role))
var userService = _services.GetRequiredService<IUserService>();
var isValid = await userService.IsAdminUser(_user.Id);
if (!isValid)
{
return "Unauthorized user.";
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Repositories.Enums;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Models;
using System.IO;
namespace BotSharp.Core.Agents.Services;
@ -12,12 +13,10 @@ public partial class AgentService
if (agent == null || string.IsNullOrEmpty(agent.Id)) return;
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
var auth = await userService.GetUserAuthorizations(new List<string> { agent.Id });
var allowEdit = auth.IsAgentActionAllowed(agent.Id, UserAction.Edit);
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)))
if (!allowEdit)
{
return;
}

View file

@ -11,6 +11,7 @@ using BotSharp.Abstraction.Infrastructures;
using BotSharp.Core.Processors;
using StackExchange.Redis;
using BotSharp.Core.Infrastructures.Events;
using BotSharp.Core.Roles.Services;
namespace BotSharp.Core;
@ -25,6 +26,7 @@ public static class BotSharpCoreExtensions
services.AddSingleton<DistributedLocker>();
services.AddScoped<ISettingService, SettingService>();
services.AddScoped<IRoleService, RoleService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<ProcessorFactory>();

View file

@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml;
namespace BotSharp.Core.Plugins;
@ -132,6 +133,12 @@ public class PluginLoader
plugins = plugins.Where(x => filter.Names.Any(n => x.Name.IsEqualTo(n))).ToList();
}
if (!string.IsNullOrEmpty(filter.SimilarName))
{
var regex = new Regex(filter.SimilarName, RegexOptions.Compiled | RegexOptions.IgnoreCase);
plugins = plugins.Where(x => regex.IsMatch(x.Name)).ToList();
}
return new PagedItems<PluginDef>
{
Items = plugins.Skip(pager.Offset).Take(pager.Size),

View file

@ -1,8 +1,6 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Users.Models;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Text.RegularExpressions;
namespace BotSharp.Core.Repository
{
@ -65,6 +63,8 @@ namespace BotSharp.Core.Repository
default:
break;
}
_agents = [];
}
#region Update Agent Fields
@ -358,12 +358,23 @@ namespace BotSharp.Core.Repository
public List<Agent> GetAgents(AgentFilter filter)
{
if (filter == null)
{
filter = AgentFilter.Empty();
}
var query = Agents;
if (!string.IsNullOrEmpty(filter.AgentName))
{
query = query.Where(x => x.Name.ToLower() == filter.AgentName.ToLower());
}
if (!string.IsNullOrEmpty(filter.SimilarName))
{
var regex = new Regex(filter.SimilarName, RegexOptions.Compiled | RegexOptions.IgnoreCase);
query = query.Where(x => regex.IsMatch(x.Name));
}
if (filter.Disabled.HasValue)
{
query = query.Where(x => x.Disabled == filter.Disabled);
@ -476,7 +487,8 @@ namespace BotSharp.Core.Repository
File.WriteAllText(instFile, agent.Instruction);
}
}
Reset();
ResetLocalAgents();
}
public void BulkInsertUserAgents(List<UserAgent> userAgents)
@ -509,7 +521,7 @@ namespace BotSharp.Core.Repository
Thread.Sleep(50);
}
Reset();
ResetLocalAgents();
}
public bool DeleteAgents()
@ -526,7 +538,7 @@ namespace BotSharp.Core.Repository
var agentDir = GetAgentDataDir(agentId);
if (string.IsNullOrEmpty(agentDir)) return false;
// Delete agent user relationships
// Delete user agents
var usersDir = Path.Combine(_dbSettings.FileRepository, USERS_FOLDER);
if (Directory.Exists(usersDir))
{
@ -544,9 +556,27 @@ namespace BotSharp.Core.Repository
}
}
// Delete role agents
var rolesDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER);
if (Directory.Exists(rolesDir))
{
foreach (var roleDir in Directory.GetDirectories(rolesDir))
{
var roleAgentFile = Directory.GetFiles(roleDir).FirstOrDefault(x => Path.GetFileName(x) == ROLE_AGENT_FILE);
if (string.IsNullOrEmpty(roleAgentFile)) continue;
var text = File.ReadAllText(roleAgentFile);
var roleAgents = JsonSerializer.Deserialize<List<RoleAgent>>(text, _options);
if (roleAgents.IsNullOrEmpty()) continue;
roleAgents = roleAgents?.Where(x => x.AgentId != agentId)?.ToList() ?? [];
File.WriteAllText(roleAgentFile, JsonSerializer.Serialize(roleAgents, _options));
}
}
// Delete agent folder
Directory.Delete(agentDir, true);
Reset();
ResetLocalAgents();
return true;
}
catch
@ -555,10 +585,11 @@ namespace BotSharp.Core.Repository
}
}
private void Reset()
private void ResetLocalAgents()
{
_agents = [];
_userAgents = [];
_roleAgents = [];
}
}
}

View file

@ -8,6 +8,11 @@ public partial class FileRepository
#region Task
public PagedItems<AgentTask> GetAgentTasks(AgentTaskFilter filter)
{
if (filter == null)
{
filter = AgentTaskFilter.Empty();
}
var tasks = new List<AgentTask>();
var pager = filter.Pager ?? new Pagination();
var skipCount = 0;

View file

@ -54,6 +54,7 @@ namespace BotSharp.Core.Repository
Directory.Delete(convDir, true);
}
return true;
}
@ -322,6 +323,11 @@ namespace BotSharp.Core.Repository
public PagedItems<Conversation> GetConversations(ConversationFilter filter)
{
if (filter == null)
{
filter = ConversationFilter.Empty();
}
var records = new List<Conversation>();
var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir);
var pager = filter?.Pager ?? new Pagination();

View file

@ -0,0 +1,143 @@
using System.IO;
namespace BotSharp.Core.Repository;
public partial class FileRepository
{
public bool RefreshRoles(IEnumerable<Role> roles)
{
if (roles.IsNullOrEmpty()) return false;
var validRoles = roles.Where(x => !string.IsNullOrWhiteSpace(x.Id)
&& !string.IsNullOrWhiteSpace(x.Name)).ToList();
if (validRoles.IsNullOrEmpty()) return false;
var baseDir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER);
if (Directory.Exists(baseDir))
{
Directory.Delete(baseDir, true);
}
Directory.CreateDirectory(baseDir);
foreach (var role in validRoles)
{
var dir = Path.Combine(baseDir, role.Id);
Directory.CreateDirectory(dir);
Thread.Sleep(50);
var roleFile = Path.Combine(dir, ROLE_FILE);
role.CreatedTime = DateTime.UtcNow;
role.UpdatedTime = DateTime.UtcNow;
File.WriteAllText(roleFile, JsonSerializer.Serialize(role, _options));
}
return true;
}
public IEnumerable<Role> GetRoles(RoleFilter filter)
{
var roles = Roles;
if (filter == null)
{
filter = RoleFilter.Empty();
}
// Apply filters
if (!filter.Names.IsNullOrEmpty())
{
roles = roles.Where(x => filter.Names.Contains(x.Name));
}
if (!filter.ExcludeRoles.IsNullOrEmpty())
{
roles = roles.Where(x => !filter.ExcludeRoles.Contains(x.Name));
}
return roles.ToList();
}
public Role? GetRoleDetails(string roleId, bool includeAgent = false)
{
if (string.IsNullOrWhiteSpace(roleId)) return null;
var role = Roles.FirstOrDefault(x => x.Id == roleId);
if (role == null) return null;
var agentActions = new List<RoleAgentAction>();
var roleAgents = RoleAgents?.Where(x => x.RoleId == roleId)?.ToList() ?? [];
if (!includeAgent)
{
agentActions = roleAgents.Select(x => new RoleAgentAction
{
Id = x.Id,
AgentId = x.AgentId,
Actions = x.Actions
}).ToList();
role.AgentActions = agentActions;
return role;
}
var agentIds = roleAgents.Select(x => x.AgentId).Distinct().ToList();
if (!agentIds.IsNullOrEmpty())
{
var agents = GetAgents(new AgentFilter { AgentIds = agentIds });
foreach (var item in roleAgents)
{
var found = agents.FirstOrDefault(x => x.Id == item.AgentId);
if (found == null) continue;
agentActions.Add(new RoleAgentAction
{
Id = item.Id,
AgentId = found.Id,
Agent = found,
Actions = item.Actions
});
}
}
role.AgentActions = agentActions;
return role;
}
public bool UpdateRole(Role role, bool updateRoleAgents = false)
{
if (string.IsNullOrEmpty(role?.Id) || string.IsNullOrEmpty(role?.Name))
{
return false;
}
var dir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER, role.Id);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var roleFile = Path.Combine(dir, ROLE_FILE);
role.CreatedTime = DateTime.UtcNow;
role.UpdatedTime = DateTime.UtcNow;
File.WriteAllText(roleFile, JsonSerializer.Serialize(role, _options));
if (updateRoleAgents)
{
var roleAgents = role.AgentActions?.Select(x => new RoleAgent
{
Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(),
RoleId = role.Id,
AgentId = x.AgentId,
Actions = x.Actions ?? [],
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow
})?.ToList() ?? [];
var roleAgentFile = Path.Combine(dir, ROLE_AGENT_FILE);
File.WriteAllText(roleAgentFile, JsonSerializer.Serialize(roleAgents, _options));
_roleAgents = [];
}
_roles = [];
return true;
}
}

View file

@ -1,7 +1,5 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Models;
using System;
using System.IO;
namespace BotSharp.Core.Repository;
@ -73,6 +71,11 @@ public partial class FileRepository
public PagedItems<User> GetUsers(UserFilter filter)
{
if (filter == null)
{
filter = UserFilter.Empty();
}
var users = Users;
// Apply filters
@ -92,39 +95,15 @@ public partial class FileRepository
{
users = users.Where(x => filter.Roles.Contains(x.Role));
}
if (!filter.Types.IsNullOrEmpty())
{
users = users.Where(x => filter.Types.Contains(x.Type));
}
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<User>
{
Items = users.OrderByDescending(x => x.CreatedTime).Skip(filter.Offset).Take(filter.Size),
@ -132,7 +111,53 @@ public partial class FileRepository
};
}
public bool UpdateUser(User user, bool isUpdateUserAgents = false)
public User? GetUserDetails(string userId, bool includeAgent = false)
{
if (string.IsNullOrWhiteSpace(userId)) return null;
var user = Users.FirstOrDefault(x => x.Id == userId || x.ExternalId == userId);
if (user == null) return null;
var agentActions = new List<UserAgentAction>();
var userAgents = UserAgents?.Where(x => x.UserId == userId)?.ToList() ?? [];
if (!includeAgent)
{
agentActions = userAgents.Select(x => new UserAgentAction
{
Id = x.Id,
AgentId = x.AgentId,
Actions = x.Actions
}).ToList();
user.AgentActions = agentActions;
return user;
}
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 found = agents.FirstOrDefault(x => x.Id == item.AgentId);
if (found == null) continue;
agentActions.Add(new UserAgentAction
{
Id = item.Id,
AgentId = found.Id,
Agent = found,
Actions = item.Actions ?? []
});
}
}
user.AgentActions = agentActions;
return user;
}
public bool UpdateUser(User user, bool updateUserAgents = false)
{
if (string.IsNullOrEmpty(user?.Id)) return false;
@ -146,7 +171,7 @@ public partial class FileRepository
user.UpdatedTime = DateTime.UtcNow;
File.WriteAllText(userFile, JsonSerializer.Serialize(user, _options));
if (isUpdateUserAgents)
if (updateUserAgents)
{
var userAgents = user.AgentActions?.Select(x => new UserAgent
{
@ -160,10 +185,10 @@ public partial class FileRepository
var userAgentFile = Path.Combine(dir, USER_AGENT_FILE);
File.WriteAllText(userAgentFile, JsonSerializer.Serialize(userAgents, _options));
_userAgents = [];
}
_users = [];
_userAgents = [];
return true;
}
}

View file

@ -22,30 +22,38 @@ public partial class FileRepository : IBotSharpRepository
private const string AGENT_FILE = "agent.json";
private const string AGENT_INSTRUCTION_FILE = "instruction";
private const string AGENT_SAMPLES_FILE = "samples.txt";
private const string USER_FILE = "user.json";
private const string USER_AGENT_FILE = "agents.json";
private const string CONVERSATION_FILE = "conversation.json";
private const string STATS_FILE = "stats.json";
private const string DIALOG_FILE = "dialogs.json";
private const string STATE_FILE = "state.json";
private const string BREAKPOINT_FILE = "breakpoint.json";
private const string EXECUTION_LOG_FILE = "execution.log";
private const string PLUGIN_CONFIG_FILE = "config.json";
private const string AGENT_TASK_PREFIX = "#metadata";
private const string AGENT_TASK_SUFFIX = "/metadata";
private const string TRANSLATION_MEMORY_FILE = "memory.json";
private const string AGENT_INSTRUCTIONS_FOLDER = "instructions";
private const string AGENT_FUNCTIONS_FOLDER = "functions";
private const string AGENT_TEMPLATES_FOLDER = "templates";
private const string AGENT_RESPONSES_FOLDER = "responses";
private const string AGENT_TASKS_FOLDER = "tasks";
private const string AGENT_TASK_PREFIX = "#metadata";
private const string AGENT_TASK_SUFFIX = "/metadata";
private const string CONVERSATION_FILE = "conversation.json";
private const string DIALOG_FILE = "dialogs.json";
private const string STATE_FILE = "state.json";
private const string BREAKPOINT_FILE = "breakpoint.json";
private const string TRANSLATION_MEMORY_FILE = "memory.json";
private const string USERS_FOLDER = "users";
private const string USER_FILE = "user.json";
private const string USER_AGENT_FILE = "agents.json";
private const string ROLES_FOLDER = "roles";
private const string ROLE_FILE = "role.json";
private const string ROLE_AGENT_FILE = "agents.json";
private const string KNOWLEDGE_FOLDER = "knowledgebase";
private const string VECTOR_FOLDER = "vector";
private const string COLLECTION_CONFIG_FILE = "collection-config.json";
private const string KNOWLEDGE_DOC_FOLDER = "document";
private const string KNOWLEDGE_DOC_META_FILE = "meta.json";
private const string EXECUTION_LOG_FILE = "execution.log";
private const string PLUGIN_CONFIG_FILE = "config.json";
private const string STATS_FILE = "stats.json";
public FileRepository(
IServiceProvider services,
BotSharpDatabaseSettings dbSettings,
@ -73,12 +81,68 @@ public partial class FileRepository : IBotSharpRepository
_dbSettings.FileRepository = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _dbSettings.FileRepository);
}
private List<Role> _roles = new List<Role>();
private List<User> _users = new List<User>();
private List<Agent> _agents = new List<Agent>();
private List<RoleAgent> _roleAgents = new List<RoleAgent>();
private List<UserAgent> _userAgents = new List<UserAgent>();
private List<Conversation> _conversations = new List<Conversation>();
private PluginConfig? _pluginConfig = null;
private IQueryable<Role> Roles
{
get
{
if (!_roles.IsNullOrEmpty())
{
return _roles.AsQueryable();
}
var dir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER);
_roles = new List<Role>();
if (Directory.Exists(dir))
{
foreach (var d in Directory.GetDirectories(dir))
{
var roleFile = Path.Combine(d, ROLE_FILE);
if (!Directory.Exists(d) || !File.Exists(roleFile))
continue;
var json = File.ReadAllText(roleFile);
_roles.Add(JsonSerializer.Deserialize<Role>(json, _options));
}
}
return _roles.AsQueryable();
}
}
private IQueryable<RoleAgent> RoleAgents
{
get
{
if (!_roleAgents.IsNullOrEmpty())
{
return _roleAgents.AsQueryable();
}
var dir = Path.Combine(_dbSettings.FileRepository, ROLES_FOLDER);
_roleAgents = new List<RoleAgent>();
if (Directory.Exists(dir))
{
foreach (var d in Directory.GetDirectories(dir))
{
var file = Path.Combine(d, ROLE_AGENT_FILE);
if (!Directory.Exists(d) || !File.Exists(file))
continue;
var json = File.ReadAllText(file);
_roleAgents.AddRange(JsonSerializer.Deserialize<List<RoleAgent>>(json, _options));
}
}
return _roleAgents.AsQueryable();
}
}
private IQueryable<User> Users
{
get

View file

@ -0,0 +1,65 @@
using BotSharp.Abstraction.Users.Enums;
using System.Reflection;
namespace BotSharp.Core.Roles.Services;
public class RoleService : IRoleService
{
private readonly IServiceProvider _services;
private readonly ILogger<RoleService> _logger;
public RoleService(
IServiceProvider services,
ILogger<RoleService> logger)
{
_services = services;
_logger = logger;
}
public async Task<bool> RefreshRoles()
{
var allRoles = await GetRoleOptions();
var roles = allRoles.Select(x => new Role { Id = Guid.NewGuid().ToString(), Name = x }).ToList();
var db = _services.GetRequiredService<IBotSharpRepository>();
return db.RefreshRoles(roles);
}
public async Task<IEnumerable<string>> GetRoleOptions()
{
var fields = typeof(UserRole).GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(x => x.IsLiteral && !x.IsInitOnly).ToList();
return fields.Select(x => x.GetValue(null)?.ToString())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList();
}
public async Task<IEnumerable<Role>> GetRoles(RoleFilter filter)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var roles = db.GetRoles(filter);
return roles;
}
public async Task<Role?> GetRoleDetails(string roleId, bool includeAgent = false)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var role = db.GetRoleDetails(roleId, includeAgent);
return role;
}
public async Task<bool> UpdateRole(Role role, bool isUpdateRoleAgents = false)
{
if (role == null) return false;
if (string.IsNullOrEmpty(role.Id))
{
role.Id = Guid.NewGuid().ToString();
}
var db = _services.GetRequiredService<IBotSharpRepository>();
return db.UpdateRole(role, isUpdateRoleAgents);
}
}

View file

@ -407,10 +407,63 @@ public class UserService : IUserService
return users;
}
public async Task<bool> UpdateUser(User model, bool isUpdateUserAgents = false)
public async Task<bool> IsAdminUser(string userId)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
return db.UpdateUser(model, isUpdateUserAgents);
var user = db.GetUserById(userId);
return user != null && UserConstant.AdminRoles.Contains(user.Role);
}
public async Task<UserAuthorization> GetUserAuthorizations(IEnumerable<string>? agentIds = null)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(_user.Id);
var auth = new UserAuthorization();
if (user == null) return auth;
auth.IsAdmin = UserConstant.AdminRoles.Contains(user.Role);
var role = db.GetRoles(new RoleFilter { Names = [user.Role] }).FirstOrDefault();
var permissions = user.Permissions?.Any() == true ? user.Permissions : role?.Permissions ?? [];
auth.Permissions = permissions;
if (agentIds == null || !agentIds.Any())
{
return auth;
}
var userAgents = db.GetUserDetails(user.Id)?.AgentActions?
.Where(x => agentIds.Contains(x.AgentId) && x.Actions.Any())?.Select(x => new UserAgent
{
AgentId = x.AgentId,
Actions = x.Actions
}).ToList() ?? [];
var userAgentIds = userAgents.Select(x => x.AgentId).ToList();
var roleAgents = db.GetRoleDetails(role?.Id)?.AgentActions?
.Where(x => !userAgentIds.Contains(x.AgentId))?.Select(x => new UserAgent
{
AgentId = x.AgentId,
Actions = x.Actions
})?.ToList() ?? [];
auth.AgentActions = userAgents.Concat(roleAgents);
return auth;
}
public async Task<User?> GetUserDetails(string userId, bool includeAgent = false)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
return db.GetUserDetails(userId, includeAgent);
}
public async Task<bool> UpdateUser(User user, bool isUpdateUserAgents = false)
{
if (user == null) return false;
var db = _services.GetRequiredService<IBotSharpRepository>();
return db.UpdateUser(user, isUpdateUserAgents);
}
public async Task<Token> ActiveUser(UserActivationModel model)

View file

@ -16,6 +16,8 @@ global using BotSharp.Abstraction.Agents;
global using BotSharp.Abstraction.Conversations;
global using BotSharp.Abstraction.Knowledges;
global using BotSharp.Abstraction.Users;
global using BotSharp.Abstraction.Roles;
global using BotSharp.Abstraction.Roles.Models;
global using BotSharp.Abstraction.Utilities;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Agents.Settings;

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.OpenAPI.Controllers;
@ -58,20 +57,13 @@ public class AgentController : ControllerBase
rule.RedirectToAgentName = found.Name;
}
var editable = true;
var chatable = true;
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
if (!UserConstant.AdminRoles.Contains(user?.Role))
{
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);
}
var auth = await userService.GetUserAuthorizations(new List<string> { targetAgent.Id });
targetAgent.Editable = editable;
targetAgent.Chatable = chatable;
targetAgent.Editable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Edit);
targetAgent.Chatable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Chat);
targetAgent.Trainable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Train);
targetAgent.Evaluable = auth.IsAgentActionAllowed(targetAgent.Id, UserAction.Evaluate);
return targetAgent;
}
@ -94,27 +86,14 @@ public class AgentController : ControllerBase
};
}
var userAgents = new List<UserAgent>();
var user = await userService.GetUser(_user.Id);
if (!UserConstant.AdminRoles.Contains(user.Role))
{
userAgents = await _agentService.GetUserAgents(user.Id);
}
var auth = await userService.GetUserAuthorizations(pagedAgents.Items.Select(x => x.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;
model.Editable = auth.IsAgentActionAllowed(x.Id, UserAction.Edit);
model.Chatable = auth.IsAgentActionAllowed(x.Id, UserAction.Chat);
model.Trainable = auth.IsAgentActionAllowed(x.Id, UserAction.Train);
model.Evaluable = auth.IsAgentActionAllowed(x.Id, UserAction.Evaluate);
return model;
})?.ToList() ?? [];

View file

@ -120,12 +120,13 @@ public class KnowledgeBaseController : ControllerBase
[HttpPost("/knowledge/document/{collection}/upload")]
public async Task<UploadKnowledgeResponse> UploadKnowledgeDocuments([FromRoute] string collection, [FromBody] VectorKnowledgeUploadRequest request)
{
var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, request.Files);
var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, request.Files, request.ChunkOption);
return response;
}
[HttpPost("/knowledge/document/{collection}/form-upload")]
public async Task<UploadKnowledgeResponse> UploadKnowledgeDocuments([FromRoute] string collection, [FromForm] IEnumerable<IFormFile> files)
public async Task<UploadKnowledgeResponse> UploadKnowledgeDocuments([FromRoute] string collection,
[FromForm] IEnumerable<IFormFile> files, [FromForm] ChunkOption? option = null)
{
if (files.IsNullOrEmpty())
{
@ -143,7 +144,7 @@ public class KnowledgeBaseController : ControllerBase
});
}
var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, docs);
var response = await _knowledgeService.UploadDocumentsToKnowledge(collection, docs, option);
return response;
}

View file

@ -22,9 +22,8 @@ public class PluginController : ControllerBase
[HttpGet("/plugins")]
public async Task<PagedItems<PluginDef>> GetPlugins([FromQuery] PluginFilter filter)
{
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
if (!UserConstant.AdminRoles.Contains(user?.Role))
var isValid = await IsValidUser();
if (!isValid)
{
return new PagedItems<PluginDef>();
}
@ -55,7 +54,11 @@ public class PluginController : ControllerBase
{
Roles = new List<string> { UserRole.Root, UserRole.Admin }
},
new PluginMenuDef("Users", link: "page/users", icon: "bx bx-user", weight: 33)
new PluginMenuDef("Roles", link: "page/roles", icon: "bx bx-group", weight: 33)
{
Roles = new List<string> { UserRole.Root, UserRole.Admin }
},
new PluginMenuDef("Users", link: "page/users", icon: "bx bx-user", weight: 34)
{
Roles = new List<string> { UserRole.Root, UserRole.Admin }
}
@ -91,4 +94,10 @@ public class PluginController : ControllerBase
var loader = _services.GetRequiredService<PluginLoader>();
return loader.UpdatePluginStatus(_services, id, false);
}
private async Task<bool> IsValidUser()
{
var userService = _services.GetRequiredService<IUserService>();
return await userService.IsAdminUser(_user.Id);
}
}

View file

@ -0,0 +1,88 @@
using BotSharp.Abstraction.Roles;
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.OpenAPI.Controllers;
[Authorize]
[ApiController]
public class RoleController : ControllerBase
{
private readonly IServiceProvider _services;
private readonly IRoleService _roleService;
private readonly IUserIdentity _user;
public RoleController(
IServiceProvider services,
IRoleService roleService,
IUserIdentity user)
{
_services = services;
_roleService = roleService;
_user = user;
}
[HttpPost("/role/refresh")]
public async Task<bool> RefreshRoles()
{
var isValid = await IsValidUser();
if (!isValid)
{
return false;
}
return await _roleService.RefreshRoles();
}
[HttpGet("/role/options")]
public async Task<IEnumerable<string>> GetRoleOptions()
{
return await _roleService.GetRoleOptions();
}
[HttpPost("/roles")]
public async Task<IEnumerable<RoleViewModel>> GetRoles([FromBody] RoleFilter? filter = null)
{
if (filter == null)
{
filter = RoleFilter.Empty();
}
var isValid = await IsValidUser();
if (!isValid)
{
return Enumerable.Empty<RoleViewModel>();
}
var roles = await _roleService.GetRoles(filter);
return roles.Select(x => RoleViewModel.FromRole(x)).ToList();
}
[HttpGet("/role/{id}/details")]
public async Task<RoleViewModel> GetRoleDetails([FromRoute] string id)
{
var role = await _roleService.GetRoleDetails(id, includeAgent: true);
return RoleViewModel.FromRole(role);
}
[HttpPut("/role")]
public async Task<bool> UpdateRole([FromBody] RoleUpdateModel model)
{
if (model == null) return false;
var isValid = await IsValidUser();
if (!isValid)
{
return false;
}
var role = RoleUpdateModel.ToRole(model);
return await _roleService.UpdateRole(role, isUpdateRoleAgents: true);
}
private async Task<bool> IsValidUser()
{
var userService = _services.GetRequiredService<IUserService>();
return await userService.IsAdminUser(_user.Id);
}
}

View file

@ -182,8 +182,8 @@ public class UserController : ControllerBase
public async Task<PagedItems<UserViewModel>> GetUsers([FromBody] UserFilter filter)
{
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
if (user == null || !UserConstant.AdminRoles.Contains(user.Role))
var isValid = await IsValidUser();
if (!isValid)
{
return new PagedItems<UserViewModel>();
}
@ -198,19 +198,26 @@ public class UserController : ControllerBase
};
}
[HttpGet("/user/{id}/details")]
public async Task<UserViewModel> GetUserDetails(string id)
{
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUserDetails(id, includeAgent: true);
return UserViewModel.FromUser(user);
}
[HttpPut("/user")]
public async Task<bool> UpdateUser([FromBody] UserUpdateModel model)
{
if (model == null) return false;
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
if (user == null || !UserConstant.AdminRoles.Contains(user.Role))
var isValid = await IsValidUser();
if (!isValid)
{
return false;
}
var userService = _services.GetRequiredService<IUserService>();
var updated = await userService.UpdateUser(UserUpdateModel.ToUser(model), isUpdateUserAgents: true);
return updated;
}
@ -245,6 +252,12 @@ public class UserController : ControllerBase
#region Private methods
private async Task<bool> IsValidUser()
{
var userService = _services.GetRequiredService<IUserService>();
return await userService.IsAdminUser(_user.Id);
}
private FileContentResult BuildFileResult(string file)
{
var fileStorage = _services.GetRequiredService<IFileStorageService>();

View file

@ -32,3 +32,4 @@ global using BotSharp.OpenAPI.ViewModels.Conversations;
global using BotSharp.OpenAPI.ViewModels.Users;
global using BotSharp.OpenAPI.ViewModels.Agents;
global using BotSharp.OpenAPI.ViewModels.Files;
global using BotSharp.OpenAPI.ViewModels.Roles;

View file

@ -48,6 +48,8 @@ public class AgentViewModel
public bool Editable { get; set; }
public bool Chatable { get; set; }
public bool Trainable { get; set; }
public bool Evaluable { get; set; }
[JsonPropertyName("created_datetime")]
public DateTime CreatedDateTime { get; set; }

View file

@ -6,4 +6,7 @@ public class VectorKnowledgeUploadRequest
{
[JsonPropertyName("files")]
public IEnumerable<ExternalFileModel> Files { get; set; } = new List<ExternalFileModel>();
[JsonPropertyName("chunk_option")]
public ChunkOption? ChunkOption { get; set; }
}

View file

@ -0,0 +1,42 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Roles.Models;
using System.Text.Json.Serialization;
namespace BotSharp.OpenAPI.ViewModels.Roles;
public class RoleAgentActionViewModel
{
[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<string> Actions { get; set; } = [];
public static RoleAgentActionViewModel ToViewModel(RoleAgentAction action)
{
return new RoleAgentActionViewModel
{
Id = action.Id,
AgentId = action.AgentId,
Agent = action.Agent,
Actions = action.Actions
};
}
public static RoleAgentAction ToDomainModel(RoleAgentActionViewModel action)
{
return new RoleAgentAction
{
Id = action.Id,
AgentId = action.AgentId,
Actions = action.Actions
};
}
}

View file

@ -0,0 +1,30 @@
using BotSharp.Abstraction.Roles.Models;
using System.Text.Json.Serialization;
namespace BotSharp.OpenAPI.ViewModels.Roles;
public class RoleUpdateModel
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("permissions")]
public IEnumerable<string> Permissions { get; set; } = [];
[JsonPropertyName("agent_actions")]
public IEnumerable<RoleAgentActionViewModel> AgentActions { get; set; } = [];
public static Role ToRole(RoleUpdateModel model)
{
return new Role
{
Id = model.Id,
Name = model.Name,
Permissions = model.Permissions,
AgentActions = model.AgentActions?.Select(x => RoleAgentActionViewModel.ToDomainModel(x)) ?? []
};
}
}

View file

@ -0,0 +1,40 @@
using BotSharp.Abstraction.Roles.Models;
using System.Text.Json.Serialization;
namespace BotSharp.OpenAPI.ViewModels.Roles;
public class RoleViewModel
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("permissions")]
public IEnumerable<string> Permissions { get; set; } = [];
[JsonPropertyName("agent_actions")]
public IEnumerable<RoleAgentActionViewModel> AgentActions { get; set; } = [];
[JsonPropertyName("create_date")]
public DateTime? CreateDate { get; set; }
[JsonPropertyName("update_date")]
public DateTime? UpdateDate { get; set; }
public static RoleViewModel FromRole(Role? role)
{
if (role == null) return null;
return new RoleViewModel
{
Id = role.Id,
Name = role.Name,
Permissions = role.Permissions,
AgentActions = role.AgentActions?.Select(x => RoleAgentActionViewModel.ToViewModel(x)) ?? [],
CreateDate = role.CreatedTime != default ? role.CreatedTime : null,
UpdateDate = role.UpdatedTime != default ? role.UpdatedTime : null
};
}
}

View file

@ -10,7 +10,8 @@ namespace BotSharp.Plugin.KnowledgeBase.Services;
public partial class KnowledgeService
{
public async Task<UploadKnowledgeResponse> UploadDocumentsToKnowledge(string collectionName, IEnumerable<ExternalFileModel> files)
public async Task<UploadKnowledgeResponse> UploadDocumentsToKnowledge(string collectionName,
IEnumerable<ExternalFileModel> files, ChunkOption? option = null)
{
var res = new UploadKnowledgeResponse
{
@ -48,7 +49,7 @@ public partial class KnowledgeService
{
// Get document info
var (contentType, bytes) = await GetFileInfo(file);
var contents = await GetFileContent(contentType, bytes);
var contents = await GetFileContent(contentType, bytes, option ?? ChunkOption.Default());
// Save document
var fileId = Guid.NewGuid();
@ -369,13 +370,13 @@ public partial class KnowledgeService
}
#region Read doc content
private async Task<IEnumerable<string>> GetFileContent(string contentType, byte[] bytes)
private async Task<IEnumerable<string>> GetFileContent(string contentType, byte[] bytes, ChunkOption option)
{
IEnumerable<string> results = new List<string>();
if (contentType.IsEqualTo(MediaTypeNames.Text.Plain))
{
results = await ReadTxt(bytes);
results = await ReadTxt(bytes, option);
}
else if (contentType.IsEqualTo(MediaTypeNames.Application.Pdf))
{
@ -385,7 +386,7 @@ public partial class KnowledgeService
return results;
}
private async Task<IEnumerable<string>> ReadTxt(byte[] bytes)
private async Task<IEnumerable<string>> ReadTxt(byte[] bytes, ChunkOption option)
{
using var stream = new MemoryStream(bytes);
using var reader = new StreamReader(stream);
@ -393,12 +394,7 @@ public partial class KnowledgeService
reader.Close();
stream.Close();
var lines = TextChopper.Chop(content, new ChunkOption
{
Size = 1024,
Conjunction = 12,
SplitByWord = true,
});
var lines = TextChopper.Chop(content, option);
return lines;
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LangChain.Providers.Google.VertexAI" Version="0.15.3-dev.58" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,71 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MLTasks;
using LangChain.Providers;
using LangChain.Providers.Google.VertexAI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BotSharp.Plugin.VertexAI.Providers
{
public class ChatCompletionProvider(VertexAIConfiguration config,
ChatSettings settings,
ILogger<TextCompletionProvider> logger,
IServiceProvider services) : IChatCompletion
{
public string Provider => "vertexai";
private readonly VertexAIConfiguration _config = config;
private readonly ChatSettings? _settings = settings;
private readonly IServiceProvider _services = services;
private readonly ILogger _logger = logger;
public required string _model;
public void SetModelName(string model)
{
_model = model;
}
public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
{
var hooks = _services.GetServices<IContentGeneratingHook>().ToList();
Task.WaitAll(hooks.Select(hook =>
hook.BeforeGenerating(agent, conversations)).ToArray());
var client = new VertexAIProvider(_config);
var model = new VertexAIChatModel(client, _model);
var messages = conversations
.Select(c => new Message(c.Content, c.Role == AgentRole.User ? MessageRole.Human : MessageRole.Ai)).ToList();
var response = await model.GenerateAsync(new ChatRequest { Messages = messages }, _settings);
var msg = new RoleDialogModel(MessageRole.Ai.ToString(), response.LastMessageContent)
{
CurrentAgentId = agent.Id
};
Task.WaitAll(hooks.Select(hook =>
hook.AfterGenerated(msg, new TokenStatsModel
{
Prompt = response.Messages[0].Content,
Model = _model
})).ToArray());
return msg;
}
public Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting)
{
throw new NotImplementedException();
}
public Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,64 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MLTasks;
using LangChain.Providers;
using LangChain.Providers.Google.VertexAI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace BotSharp.Plugin.VertexAI.Providers
{
public class TextCompletionProvider(VertexAIConfiguration config,
ChatSettings settings,
ILogger<TextCompletionProvider> logger,
IServiceProvider services) : ITextCompletion
{
public string Provider => "vertexai";
private readonly VertexAIConfiguration _config = config;
private readonly ChatSettings? _settings = settings;
private readonly IServiceProvider _services = services;
private readonly ILogger _logger = logger;
public required string _model;
public async Task<string> GetCompletion(string text, string agentId, string messageId)
{
var contentHooks = _services.GetServices<IContentGeneratingHook>().ToList();
var agent = new Agent()
{
Id = agentId,
};
var client = new VertexAIProvider(_config);
var model = new VertexAIChatModel(client, _model);
var response = await model.GenerateAsync(text, _settings);
var responseMessage = new RoleDialogModel(AgentRole.Assistant, response.LastMessageContent)
{
CurrentAgentId = agentId,
MessageId = messageId
};
Task.WaitAll(contentHooks.Select(hook =>
hook.AfterGenerated(responseMessage, new TokenStatsModel
{
Prompt = text,
Provider = Provider,
Model = _model,
PromptCount = response.Usage.TotalTokens,
CompletionCount = response.Usage.OutputTokens
})).ToArray());
return response.LastMessageContent;
}
public void SetModelName(string model)
{
_model = model;
}
}
}

View file

@ -0,0 +1,28 @@
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Plugins;
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.VertexAI.Providers;
using LangChain.Providers.Google.VertexAI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace BotSharp.Plugin.VertexAI;
public class VertexAiPlugin : IBotSharpPlugin
{
public string Id => "962ff441-2b40-4db4-b530-49efb1688a75";
public string Name => "VertexAI";
public string Description => "VertexAI Service including text generation, text to image and other AI services.";
public string IconUrl => "https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Vertex_AI_Logo.svg/480px-Vertex_AI_Logo.svg.png";
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
services.AddScoped(provider =>
{
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<VertexAIConfiguration>("VertexAI");
});
services.AddScoped<IChatCompletion, ChatCompletionProvider>();
services.AddScoped<ITextCompletion, TextCompletionProvider>();
}
}

View file

@ -0,0 +1,25 @@
using BotSharp.Abstraction.Roles.Models;
namespace BotSharp.Plugin.MongoStorage.Collections;
public class RoleAgentDocument : MongoBase
{
public string RoleId { get; set; }
public string AgentId { get; set; }
public IEnumerable<string> Actions { get; set; } = [];
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public RoleAgent ToRoleAgent()
{
return new RoleAgent
{
Id = Id,
RoleId = RoleId,
AgentId = AgentId,
Actions = Actions,
CreatedTime = CreatedTime,
UpdatedTime = UpdatedTime
};
}
}

View file

@ -0,0 +1,24 @@
using BotSharp.Abstraction.Roles.Models;
namespace BotSharp.Plugin.MongoStorage.Collections;
public class RoleDocument : MongoBase
{
public string Name { get; set; }
public IEnumerable<string> Permissions { get; set; } = [];
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public Role ToRole()
{
return new Role
{
Id = Id,
Name = Name,
Permissions = Permissions,
CreatedTime = CreatedTime,
UpdatedTime = UpdatedTime
};
}
}

View file

@ -159,4 +159,10 @@ public class MongoDbContext
public IMongoCollection<KnowledgeCollectionFileMetaDocument> KnowledgeCollectionFileMeta
=> Database.GetCollection<KnowledgeCollectionFileMetaDocument>($"{_collectionPrefix}_KnowledgeCollectionFileMeta");
public IMongoCollection<RoleDocument> Roles
=> Database.GetCollection<RoleDocument>($"{_collectionPrefix}_Roles");
public IMongoCollection<RoleAgentDocument> RoleAgents
=> Database.GetCollection<RoleAgentDocument>($"{_collectionPrefix}_RoleAgents");
}

View file

@ -2,6 +2,7 @@ using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Functions.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Routing.Models;
using MongoDB.Driver;
namespace BotSharp.Plugin.MongoStorage.Repository;
@ -283,6 +284,11 @@ public partial class MongoRepository
public List<Agent> GetAgents(AgentFilter filter)
{
if (filter == null)
{
filter = AgentFilter.Empty();
}
var agents = new List<Agent>();
var builder = Builders<AgentDocument>.Filter;
var filters = new List<FilterDefinition<AgentDocument>>() { builder.Empty };
@ -292,6 +298,11 @@ public partial class MongoRepository
filters.Add(builder.Eq(x => x.Name, filter.AgentName));
}
if (!string.IsNullOrEmpty(filter.SimilarName))
{
filters.Add(builder.Regex(x => x.Name, new BsonRegularExpression(filter.SimilarName, "i")));
}
if (filter.Disabled.HasValue)
{
filters.Add(builder.Eq(x => x.Disabled, filter.Disabled.Value));
@ -327,8 +338,6 @@ public partial class MongoRepository
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
{
Id = x.Id,
@ -339,6 +348,8 @@ public partial class MongoRepository
UpdatedTime = x.UpdatedTime
}).ToList();
var agentIds = found.Select(x => x.AgentId).Distinct().ToList();
var agents = GetAgents(new AgentFilter { AgentIds = agentIds });
foreach (var item in res)
{
var agent = agents.FirstOrDefault(x => x.Id == item.AgentId);
@ -450,6 +461,7 @@ public partial class MongoRepository
try
{
_dc.UserAgents.DeleteMany(Builders<UserAgentDocument>.Filter.Empty);
_dc.RoleAgents.DeleteMany(Builders<RoleAgentDocument>.Filter.Empty);
_dc.Agents.DeleteMany(Builders<AgentDocument>.Filter.Empty);
return true;
}
@ -467,10 +479,12 @@ public partial class MongoRepository
var agentFilter = Builders<AgentDocument>.Filter.Eq(x => x.Id, agentId);
var userAgentFilter = Builders<UserAgentDocument>.Filter.Eq(x => x.AgentId, agentId);
var roleAgentFilter = Builders<RoleAgentDocument>.Filter.Eq(x => x.AgentId, agentId);
var agentTaskFilter = Builders<AgentTaskDocument>.Filter.Eq(x => x.AgentId, agentId);
_dc.Agents.DeleteOne(agentFilter);
_dc.UserAgents.DeleteMany(userAgentFilter);
_dc.RoleAgents.DeleteMany(roleAgentFilter);
_dc.AgentTasks.DeleteMany(agentTaskFilter);
return true;
}

View file

@ -8,6 +8,11 @@ public partial class MongoRepository
#region Task
public PagedItems<AgentTask> GetAgentTasks(AgentTaskFilter filter)
{
if (filter == null)
{
filter = AgentTaskFilter.Empty();
}
var pager = filter.Pager ?? new Pagination();
var builder = Builders<AgentTaskDocument>.Filter;
var filters = new List<FilterDefinition<AgentTaskDocument>>() { builder.Empty };

View file

@ -281,6 +281,11 @@ public partial class MongoRepository
public PagedItems<Conversation> GetConversations(ConversationFilter filter)
{
if (filter == null)
{
filter = ConversationFilter.Empty();
}
var convBuilder = Builders<ConversationDocument>.Filter;
var convFilters = new List<FilterDefinition<ConversationDocument>>() { convBuilder.Empty };

View file

@ -0,0 +1,157 @@
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Roles.Models;
namespace BotSharp.Plugin.MongoStorage.Repository;
public partial class MongoRepository
{
public bool RefreshRoles(IEnumerable<Role> roles)
{
if (roles.IsNullOrEmpty()) return false;
var validRoles = roles.Where(x => !string.IsNullOrWhiteSpace(x.Id)
&& !string.IsNullOrWhiteSpace(x.Name)).ToList();
if (validRoles.IsNullOrEmpty()) return false;
// Clear data
_dc.RoleAgents.DeleteMany(Builders<RoleAgentDocument>.Filter.Empty);
_dc.Roles.DeleteMany(Builders<RoleDocument>.Filter.Empty);
var roleDocs = validRoles.Select(x => new RoleDocument
{
Id = x.Id,
Name = x.Name,
Permissions = x.Permissions,
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow
});
_dc.Roles.InsertMany(roleDocs);
return true;
}
public IEnumerable<Role> GetRoles(RoleFilter filter)
{
if (filter == null)
{
filter = RoleFilter.Empty();
}
var roleBuilder = Builders<RoleDocument>.Filter;
var roleFilters = new List<FilterDefinition<RoleDocument>>() { roleBuilder.Empty };
// Apply filters
if (!filter.Names.IsNullOrEmpty())
{
roleFilters.Add(roleBuilder.In(x => x.Name, filter.Names));
}
if (!filter.ExcludeRoles.IsNullOrEmpty())
{
roleFilters.Add(roleBuilder.Nin(x => x.Name, filter.ExcludeRoles));
}
// Search
var roleDocs = _dc.Roles.Find(roleBuilder.And(roleFilters)).ToList();
var roles = roleDocs.Select(x => x.ToRole()).ToList();
return roles;
}
public Role? GetRoleDetails(string roleId, bool includeAgent = false)
{
if (string.IsNullOrWhiteSpace(roleId)) return null;
var roleDoc = _dc.Roles.Find(Builders<RoleDocument>.Filter.Eq(x => x.Id, roleId)).FirstOrDefault();
if (roleDoc == null) return null;
var agentActions = new List<RoleAgentAction>();
var role = roleDoc.ToRole();
var roleAgentDocs = _dc.RoleAgents.Find(Builders<RoleAgentDocument>.Filter.Eq(x => x.RoleId, roleId)).ToList();
if (!includeAgent)
{
agentActions = roleAgentDocs.Select(x => new RoleAgentAction
{
Id = x.Id,
AgentId = x.AgentId,
Actions = x.Actions
}).ToList();
role.AgentActions = agentActions;
return role;
}
var agentIds = roleAgentDocs.Select(x => x.AgentId).Distinct().ToList();
if (!agentIds.IsNullOrEmpty())
{
var agents = GetAgents(new AgentFilter { AgentIds = agentIds });
foreach (var item in roleAgentDocs)
{
var found = agents.FirstOrDefault(x => x.Id == item.AgentId);
if (found == null) continue;
agentActions.Add(new RoleAgentAction
{
Id = item.Id,
AgentId = found.Id,
Agent = found,
Actions = item.Actions
});
}
}
role.AgentActions = agentActions;
return role;
}
public bool UpdateRole(Role role, bool updateRoleAgents = false)
{
if (string.IsNullOrEmpty(role?.Id)) return false;
var roleFilter = Builders<RoleDocument>.Filter.Eq(x => x.Id, role.Id);
var roleUpdate = Builders<RoleDocument>.Update
.Set(x => x.Name, role.Name)
.Set(x => x.Permissions, role.Permissions)
.Set(x => x.CreatedTime, DateTime.UtcNow)
.Set(x => x.UpdatedTime, DateTime.UtcNow);
_dc.Roles.UpdateOne(roleFilter, roleUpdate, _options);
if (updateRoleAgents)
{
var roleAgentDocs = role.AgentActions?.Select(x => new RoleAgentDocument
{
Id = !string.IsNullOrEmpty(x.Id) ? x.Id : Guid.NewGuid().ToString(),
RoleId = role.Id,
AgentId = x.AgentId,
Actions = x.Actions,
CreatedTime = DateTime.UtcNow,
UpdatedTime = DateTime.UtcNow
})?.ToList() ?? [];
var toDelete = _dc.RoleAgents.Find(Builders<RoleAgentDocument>.Filter.And(
Builders<RoleAgentDocument>.Filter.Eq(x => x.RoleId, role.Id),
Builders<RoleAgentDocument>.Filter.Nin(x => x.Id, roleAgentDocs.Select(x => x.Id))
)).ToList();
_dc.RoleAgents.DeleteMany(Builders<RoleAgentDocument>.Filter.In(x => x.Id, toDelete.Select(x => x.Id)));
foreach (var doc in roleAgentDocs)
{
var roleAgentFilter = Builders<RoleAgentDocument>.Filter.Eq(x => x.Id, doc.Id);
var roleAgentUpdate = Builders<RoleAgentDocument>.Update
.Set(x => x.Id, doc.Id)
.Set(x => x.RoleId, role.Id)
.Set(x => x.AgentId, doc.AgentId)
.Set(x => x.Actions, doc.Actions)
.Set(x => x.UpdatedTime, DateTime.UtcNow);
_dc.RoleAgents.UpdateOne(roleAgentFilter, roleAgentUpdate, _options);
}
}
return true;
}
}

View file

@ -2,6 +2,8 @@ using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Models;
using MongoDB.Driver;
using System.Globalization;
namespace BotSharp.Plugin.MongoStorage.Repository;
@ -15,7 +17,9 @@ public partial class MongoRepository
public User? GetUserByPhone(string phone, string role = null, string regionCode = "CN")
{
if (string.IsNullOrWhiteSpace(phone))
string phoneSecond = string.Empty;
// if phone number length is less than 4, return null
if (string.IsNullOrWhiteSpace(phone) || phone?.Length < 4)
{
return null;
}
@ -36,22 +40,19 @@ public partial class MongoRepository
public User? GetUserById(string id)
{
var user = _dc.Users.AsQueryable()
.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id));
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id));
return user != null ? user.ToUser() : null;
}
public List<User> GetUserByIds(List<string> ids)
{
var users = _dc.Users.AsQueryable()
.Where(x => ids.Contains(x.Id) || (x.ExternalId != null && ids.Contains(x.ExternalId))).ToList();
var users = _dc.Users.AsQueryable().Where(x => ids.Contains(x.Id) || (x.ExternalId != null && ids.Contains(x.ExternalId))).ToList();
return users?.Any() == true ? users.Select(x => x.ToUser()).ToList() : new List<User>();
}
public List<User> GetUsersByAffiliateId(string affiliateId)
{
var users = _dc.Users.AsQueryable()
.Where(x => x.AffiliateId == affiliateId).ToList();
var users = _dc.Users.AsQueryable().Where(x => x.AffiliateId == affiliateId).ToList();
return users?.Any() == true ? users.Select(x => x.ToUser()).ToList() : new List<User>();
}
@ -168,6 +169,11 @@ public partial class MongoRepository
public PagedItems<User> GetUsers(UserFilter filter)
{
if (filter == null)
{
filter = UserFilter.Empty();
}
var userBuilder = Builders<UserDocument>.Filter;
var userFilters = new List<FilterDefinition<UserDocument>>() { userBuilder.Empty };
@ -188,6 +194,10 @@ public partial class MongoRepository
{
userFilters.Add(userBuilder.In(x => x.Role, filter.Roles));
}
if (!filter.Types.IsNullOrEmpty())
{
userFilters.Add(userBuilder.In(x => x.Type, filter.Types));
}
if (!filter.Sources.IsNullOrEmpty())
{
userFilters.Add(userBuilder.In(x => x.Source, filter.Sources));
@ -202,44 +212,6 @@ public partial class MongoRepository
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<string>(),
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<User>
{
Items = users,
@ -247,8 +219,60 @@ public partial class MongoRepository
};
}
public User? GetUserDetails(string userId, bool includeAgent = false)
{
if (string.IsNullOrWhiteSpace(userId)) return null;
public bool UpdateUser(User user, bool isUpdateUserAgents = false)
var userDoc = _dc.Users.AsQueryable().FirstOrDefault(x => x.Id == userId || x.ExternalId == userId);
if (userDoc == null) return null;
var agentActions = new List<UserAgentAction>();
var user = userDoc.ToUser();
var userAgents = _dc.UserAgents.AsQueryable().Where(x => x.UserId == userId).Select(x => new UserAgent
{
Id = x.Id,
UserId = x.UserId,
AgentId = x.AgentId,
Actions = x.Actions ?? Enumerable.Empty<string>()
}).ToList();
if (!includeAgent)
{
agentActions = userAgents.Select(x => new UserAgentAction
{
Id = x.Id,
AgentId = x.AgentId,
Actions = x.Actions
}).ToList();
user.AgentActions = agentActions;
return user;
}
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 found = agents.FirstOrDefault(x => x.Id == item.AgentId);
if (found == null) continue;
agentActions.Add(new UserAgentAction
{
Id = item.Id,
AgentId = found.Id,
Agent = found,
Actions = item.Actions
});
}
}
user.AgentActions = agentActions;
return user;
}
public bool UpdateUser(User user, bool updateUserAgents = false)
{
if (string.IsNullOrEmpty(user?.Id)) return false;
@ -261,7 +285,7 @@ public partial class MongoRepository
_dc.Users.UpdateOne(userFilter, userUpdate);
if (isUpdateUserAgents)
if (updateUserAgents)
{
var userAgentDocs = user.AgentActions?.Select(x => new UserAgentDocument
{

View file

@ -95,6 +95,8 @@ public class SecondaryStagePlanFn : IFunctionCallback
var conv = _services.GetRequiredService<IConversationService>();
var wholeDialogs = conv.GetDialogHistory();
wholeDialogs.Last().Content += "\r\nOutput in JSON format.";
var completion = CompletionProvider.GetChatCompletion(_services,
provider: plannerAgent.LlmConfig.Provider,
model: plannerAgent.LlmConfig.Model);

View file

@ -72,8 +72,12 @@ public class SummaryPlanFn : IFunctionCallback
message.Content = summary.Content;
// Validate the sql result
await fn.InvokeFunction("validate_sql", message);
var args = JsonSerializer.Deserialize<SummaryPlan>(message.FunctionArgs);
if (args.IsSqlTemplate == false)
{
await fn.InvokeFunction("validate_sql", message);
}
await HookEmitter.Emit<IPlanningHook>(_services, async hook =>
await hook.OnPlanningCompleted(nameof(TwoStageTaskPlanner), message)
);

View file

@ -0,0 +1,7 @@
namespace BotSharp.Plugin.Planner.TwoStaging.Models;
public class SummaryPlan
{
[JsonPropertyName("is_sql_template")]
public bool IsSqlTemplate { get; set; } = false;
}

View file

@ -4,6 +4,10 @@
"parameters": {
"type": "object",
"properties": {
"is_sql_template": {
"type": "boolean",
"description": "If user request is to generate sql template instead of actual sql statement."
},
"related_tables": {
"type": "array",
"description": "table name in planning steps",
@ -13,6 +17,6 @@
}
}
},
"required": [ "related_tables" ]
"required": [ "related_tables", "is_sql_template" ]
}
}

View file

@ -1,4 +1,4 @@
You are a planning summarizer. You will generate the final output in JSON format based on the task description, knowledge and related table structure and relationship.
You are a planning summarizer. You will generate the final output in JSON format with short explanation based on the task description, knowledge and related table structure and relationship.
Requirements:
{{ summary_requirements }}

View file

@ -30,7 +30,7 @@ public class ExecuteQueryFn : IFunctionCallback
public async Task<bool> Execute(RoleDialogModel message)
{
var args = JsonSerializer.Deserialize<ExecuteQueryArgs>(message.FunctionArgs);
var refinedArgs = await RefineSqlStatement(message, args);
//var refinedArgs = await RefineSqlStatement(message, args);
var dbHook = _services.GetRequiredService<ISqlDriverHook>();
var dbType = dbHook.GetDatabaseType(message);
@ -38,13 +38,13 @@ public class ExecuteQueryFn : IFunctionCallback
{
var results = dbType.ToLower() switch
{
"mysql" => RunQueryInMySql(refinedArgs.SqlStatements),
"sqlserver" => RunQueryInSqlServer(refinedArgs.SqlStatements),
"redshift" => RunQueryInRedshift(refinedArgs.SqlStatements),
"mysql" => RunQueryInMySql(args.SqlStatements),
"sqlserver" => RunQueryInSqlServer(args.SqlStatements),
"redshift" => RunQueryInRedshift(args.SqlStatements),
_ => throw new NotImplementedException($"Database type {dbType} is not supported.")
};
if (refinedArgs.SqlStatements.Length == 1 && refinedArgs.SqlStatements[0].StartsWith("DROP TABLE"))
if (args.SqlStatements.Length == 1 && args.SqlStatements[0].StartsWith("DROP TABLE"))
{
message.Content = "Drop table successfully";
return true;

View file

@ -41,7 +41,7 @@ public class SqlValidateFn : IFunctionCallback
var dbType = dbHook.GetDatabaseType(message);
var validateSql = dbType.ToLower() switch
{
"mysql" => $"explain\r\n{sql}",
"mysql" => $"explain\r\n{sql.Replace("SET ", "-- SET ", StringComparison.InvariantCultureIgnoreCase).Replace(";", "; explain ").TrimEnd("explain ".ToCharArray())}",
"sqlserver" => $"SET PARSEONLY ON;\r\n{sql}\r\nSET PARSEONLY OFF;",
"redshift" => $"explain\r\n{sql}",
_ => throw new NotImplementedException($"Database type {dbType} is not supported.")
@ -49,7 +49,7 @@ public class SqlValidateFn : IFunctionCallback
var msgCopy = RoleDialogModel.From(message);
msgCopy.FunctionArgs = JsonSerializer.Serialize(new ExecuteQueryArgs
{
SqlStatements = new string[] { validateSql }
SqlStatements = [validateSql]
});
var fn = _services.GetRequiredService<IRoutingService>();
@ -74,7 +74,7 @@ public class SqlValidateFn : IFunctionCallback
Message = "Correct SQL Statement",
Data = new Dictionary<string, object>
{
{ "original_sql", sql },
{ "original_sql", message.Content },
{ "error_message", ex.Message },
{ "table_structure", ddl }
}

View file

@ -1,6 +1,6 @@
{
"name": "verify_dictionary_term",
"description": "Get id from dictionary table by keyword. Call this function only if need_lookup_dictionary is true and is_insert is false. You can only query one table at a time.",
"description": "Get id from dictionary table by keyword. Call this function only if need_lookup_dictionary is true, is_table_from_knowledge is true and is_insert is false. You can only query one table at a time. The table name must come from the global/domain knowledge.",
"parameters": {
"type": "object",
"properties": {
@ -16,9 +16,13 @@
"type": "boolean",
"description": "if SQL statement is inserting."
},
"is_table_from_knowledge": {
"type": "boolean",
"description": "if table is from the global/domain knowledge."
},
"tables": {
"type": "array",
"description": "all related dictionary tables must be from related knowledge in the context",
"description": "all related dictionary tables must be from global/domain knowledge in the context",
"items": {
"type": "string",
"description": "table name from related knowledge in the context"

View file

@ -4,7 +4,7 @@ If not, generate the query step by step based on the planning.
The query must exactly based on the provided table structure. And carefully review the foreign keys to make sure you include all the accurate information.
Note: Output should be only the sql query with sql comments that can be directly run in mysql database with version 8.0.
Note: Output should be only the sql query with short sql comments and explanation that can be directly run in mysql database with version 8.0.
Don't use the sql statement that specify target table for update in FROM clause.
For example, you CAN'T write query as below:

View file

@ -1,5 +1,5 @@
You are a sql statement corrector. You will need to refer to the table structure and rewrite the original sql statement so it's using the correct information, e.g. column name.
Output the sql statement only without comments, in JSON format: {{ response_format }}
Correct the sql statement and keep only the original explanation and comments without any information related to error message{% if response_format %} in JSON format: {{ response_format }} {% endif %}.
Make sure all the column names are defined in the Table Structure.
=====