Merge branch 'SciSharp:master' into master

This commit is contained in:
hchen2020 2025-03-10 13:38:27 -05:00 committed by GitHub
commit c6fc411dd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 246 additions and 79 deletions

View file

@ -12,7 +12,7 @@ public interface IAgentService
Task<Agent> CreateAgent(Agent agent);
Task<string> RefreshAgents();
Task<PagedItems<Agent>> GetAgents(AgentFilter filter);
Task<List<IdName>> GetAgentOptions();
Task<List<IdName>> GetAgentOptions(List<string>? agentIds = null);
/// <summary>
/// Load agent configurations and trigger hooks

View file

@ -6,4 +6,5 @@ public static class MessageTypeName
public const string Notification = "notification";
public const string FunctionCall = "function";
public const string Audio = "audio";
public const string Error = "error";
}

View file

@ -61,14 +61,7 @@ public interface IConversationService
void SaveStates();
/// <summary>
/// Get conversation keys for searching
/// </summary>
/// <param name="query">search query</param>
/// <param name="convLimit">conversation limit</param>
/// <param name="preLoad">if pre-loading, then keys are not filter by the search query</param>
/// <returns></returns>
Task<List<string>> GetConversationStateSearhKeys(string query, int convLimit = 100, int keyLimit = 10, bool preload = false);
Task<List<string>> GetConversationStateSearhKeys(ConversationStateKeysFilter filter);
Task<bool> MigrateLatestStates(int batchSize = 100, int errorLimit = 10);
}

View file

@ -1,5 +1,5 @@
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Repositories.Filters;
namespace BotSharp.Abstraction.Loggers.Services;
@ -12,5 +12,6 @@ public interface ILoggerService
#region Instruction
Task<PagedItems<InstructionLogModel>> GetInstructionLogs(InstructLogFilter filter);
Task<List<string>> GetInstructionLogSearchKeys(InstructLogKeysFilter filter);
#endregion
}

View file

@ -23,9 +23,9 @@ public class ConversationFilter
/// <summary>
/// Check whether each key in the list is in the conversation states and its value equals to target value if not empty
/// </summary>
public IEnumerable<KeyValue>? States { get; set; } = [];
public List<KeyValue>? States { get; set; }
public IEnumerable<string>? Tags { get; set; } = [];
public List<string>? Tags { get; set; }
public static ConversationFilter Empty()
{

View file

@ -0,0 +1,21 @@
namespace BotSharp.Abstraction.Repositories.Filters;
public class ConversationStateKeysFilter
{
public string? Query { get; set; }
public int KeyLimit { get; set; } = 10;
public int ConvLimit { get; set; } = 100;
public bool PreLoad { get; set; }
public List<string>? AgentIds { get; set; }
public List<string>? UserIds { get; set; }
public ConversationStateKeysFilter()
{
}
public static ConversationStateKeysFilter Empty()
{
return new();
}
}

View file

@ -1,4 +1,4 @@
namespace BotSharp.Abstraction.Instructs.Models;
namespace BotSharp.Abstraction.Repositories.Filters;
public class InstructLogFilter : Pagination
{
@ -11,6 +11,6 @@ public class InstructLogFilter : Pagination
public static InstructLogFilter Empty()
{
return new InstructLogFilter();
return new();
}
}

View file

@ -0,0 +1,21 @@
namespace BotSharp.Abstraction.Repositories.Filters;
public class InstructLogKeysFilter
{
public string? Query { get; set; }
public int KeyLimit { get; set; } = 10;
public int LogLimit { get; set; } = 100;
public bool PreLoad { get; set; }
public List<string>? AgentIds { get; set; }
public List<string>? UserIds { get; set; }
public InstructLogKeysFilter()
{
}
public static InstructLogKeysFilter Empty()
{
return new();
}
}

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Repositories.Filters;
@ -149,7 +148,7 @@ public interface IBotSharpRepository : IHaveServiceProvider
=> throw new NotImplementedException();
List<string> TruncateConversation(string conversationId, string messageId, bool cleanLog = false)
=> throw new NotImplementedException();
List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
List<string> GetConversationStateSearchKeys(ConversationStateKeysFilter filter)
=> throw new NotImplementedException();
List<string> GetConversationsToMigrate(int batchSize = 100)
=> throw new NotImplementedException();
@ -182,6 +181,9 @@ public interface IBotSharpRepository : IHaveServiceProvider
PagedItems<InstructionLogModel> GetInstructionLogs(InstructLogFilter filter)
=> throw new NotImplementedException();
List<string> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
=> throw new NotImplementedException();
#endregion
#region Statistics

View file

@ -10,7 +10,7 @@ public interface IUserService
Task<PagedItems<User>> GetUsers(UserFilter filter);
Task<List<User>> SearchLoginUsers(User filter);
Task<User?> GetUserDetails(string userId, bool includeAgent = false);
Task<bool> IsAdminUser(string userId);
Task<(bool, User?)> IsAdminUser(string userId);
Task<UserAuthorization> GetUserAuthorizations(IEnumerable<string>? agentIds = null);
Task<bool> UpdateUser(User user, bool isUpdateUserAgents = false);
Task<User> CreateUser(User user);

View file

@ -30,9 +30,12 @@ public partial class AgentService
#if !DEBUG
[SharpCache(10, perInstanceCache: true)]
#endif
public async Task<List<IdName>> GetAgentOptions()
public async Task<List<IdName>> GetAgentOptions(List<string>? agentIds)
{
var agents = _db.GetAgents(AgentFilter.Empty());
var agents = _db.GetAgents(new AgentFilter
{
AgentIds = !agentIds.IsNullOrEmpty() ? agentIds : null
});
return agents?.Select(x => new IdName(x.Id, x.Name))?.OrderBy(x => x.Name)?.ToList() ?? [];
}

View file

@ -18,7 +18,7 @@ public partial class AgentService
var userIdentity = _services.GetRequiredService<IUserIdentity>();
var userService = _services.GetRequiredService<IUserService>();
var isValid = await userService.IsAdminUser(userIdentity.Id);
var (isValid, _) = await userService.IsAdminUser(userIdentity.Id);
if (!isValid)
{
return "Unauthorized user.";

View file

@ -222,17 +222,26 @@ public partial class ConversationService : IConversationService
_state.Save();
}
public async Task<List<string>> GetConversationStateSearhKeys(string query, int convLimit = 100, int keyLimit = 10, bool preload = false)
public async Task<List<string>> GetConversationStateSearhKeys(ConversationStateKeysFilter filter)
{
if (filter == null)
{
filter = ConversationStateKeysFilter.Empty();
}
var keys = new List<string>();
if (!preload && string.IsNullOrWhiteSpace(query))
if (!filter.PreLoad && string.IsNullOrWhiteSpace(filter.Query))
{
return keys;
}
var userService = _services.GetRequiredService<IUserService>();
var db = _services.GetRequiredService<IBotSharpRepository>();
keys = db.GetConversationStateSearchKeys(convUpperLimit: convLimit);
keys = preload ? keys : keys.Where(x => x.Contains(query, StringComparison.OrdinalIgnoreCase)).ToList();
return keys.OrderBy(x => x).Take(keyLimit).ToList();
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : [];
keys = db.GetConversationStateSearchKeys(filter);
keys = filter.PreLoad ? keys : keys.Where(x => x.Contains(filter.Query ?? string.Empty, StringComparison.OrdinalIgnoreCase)).ToList();
return keys.OrderBy(x => x).Take(filter.KeyLimit).ToList();
}
}

View file

@ -1,6 +1,4 @@
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Abstraction.Users.Models;
namespace BotSharp.Core.Loggers.Services;
@ -15,11 +13,10 @@ public partial class LoggerService
}
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
var isAdmin = UserConstant.AdminRoles.Contains(user?.Role);
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
if (!isAdmin && user?.Id == null) return new();
filter.UserIds = isAdmin ? [] : user?.Id != null ? [user.Id] : [];
filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : null;
var agents = new List<Agent>();
var users = new List<User>();
@ -61,4 +58,27 @@ public partial class LoggerService
Count = logs.Count
};
}
public async Task<List<string>> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
{
if (filter == null)
{
filter = InstructLogKeysFilter.Empty();
}
var keys = new List<string>();
if (!filter.PreLoad && string.IsNullOrWhiteSpace(filter.Query))
{
return keys;
}
var userService = _services.GetRequiredService<IUserService>();
var db = _services.GetRequiredService<IBotSharpRepository>();
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
filter.UserIds = !isAdmin && user?.Id != null ? [user.Id] : null;
keys = db.GetInstructionLogSearchKeys(filter);
keys = filter.PreLoad ? keys : keys.Where(x => x.Contains(filter.Query ?? string.Empty, StringComparison.OrdinalIgnoreCase)).ToList();
return keys.OrderBy(x => x).Take(filter.KeyLimit).ToList();
}
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Users.Models;
using System.IO;
namespace BotSharp.Core.Repository;
@ -647,7 +648,7 @@ public partial class FileRepository
#if !DEBUG
[SharpCache(10)]
#endif
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
public List<string> GetConversationStateSearchKeys(ConversationStateKeysFilter filter)
{
var dir = Path.Combine(_dbSettings.FileRepository, _conversationSettings.DataDir);
if (!Directory.Exists(dir)) return [];
@ -658,26 +659,29 @@ public partial class FileRepository
foreach (var d in Directory.GetDirectories(dir))
{
var convFile = Path.Combine(d, CONVERSATION_FILE);
var stateFile = Path.Combine(d, STATE_FILE);
if (!File.Exists(convFile) || !File.Exists(stateFile))
var latestStateFile = Path.Combine(d, CONV_LATEST_STATE_FILE);
if (!File.Exists(convFile) || !File.Exists(latestStateFile))
{
continue;
}
var convJson = File.ReadAllText(convFile);
var stateJson = File.ReadAllText(stateFile);
var stateJson = File.ReadAllText(latestStateFile);
var conv = JsonSerializer.Deserialize<Conversation>(convJson, _options);
var states = JsonSerializer.Deserialize<List<StateKeyValue>>(stateJson, _options);
if (conv == null || conv.DialogCount < messageLowerLimit)
var states = JsonSerializer.Deserialize<Dictionary<string, JsonDocument>>(stateJson, _options);
if (conv == null
|| states.IsNullOrEmpty()
|| (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(conv.AgentId))
|| (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(conv.UserId)))
{
continue;
}
var stateKeys = states?.Select(x => x.Key)?.Distinct()?.ToList() ?? [];
var stateKeys = states?.Select(x => x.Key)?.ToList() ?? [];
keys.AddRange(stateKeys);
count++;
if (count >= convUpperLimit)
if (count >= filter.ConvLimit)
{
break;
}

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Loggers.Models;
using System.IO;
@ -271,6 +270,44 @@ namespace BotSharp.Core.Repository
Count = logs.Count()
};
}
public List<string> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
{
var keys = new List<string>();
var baseDir = Path.Combine(_dbSettings.FileRepository, INSTRUCTION_LOG_FOLDER);
if (!Directory.Exists(baseDir))
{
return keys;
}
var count = 0;
var files = Directory.GetFiles(baseDir);
foreach (var file in files)
{
var json = File.ReadAllText(file);
var log = JsonSerializer.Deserialize<InstructionLogModel>(json, _options);
if (log == null) continue;
if (log == null
|| log.InnerStates.IsNullOrEmpty()
|| (!filter.UserIds.IsNullOrEmpty() && !filter.UserIds.Contains(log.UserId))
|| (!filter.AgentIds.IsNullOrEmpty() && !filter.AgentIds.Contains(log.AgentId)))
{
continue;
}
var stateKeys = log.InnerStates.Select(x => x.Key)?.ToList() ?? [];
keys.AddRange(stateKeys);
count++;
if (count >= filter.LogLimit)
{
break;
}
}
return keys.Distinct().ToList();
}
#endregion
#region Private methods

View file

@ -418,23 +418,24 @@ public class UserService : IUserService
return users;
}
public async Task<bool> IsAdminUser(string userId)
[SharpCache(10, perInstanceCache: true)]
public async Task<(bool, User?)> IsAdminUser(string userId)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(userId);
return user != null && UserConstant.AdminRoles.Contains(user.Role);
var isAdmin = user != null && UserConstant.AdminRoles.Contains(user.Role);
return (isAdmin, user);
}
public async Task<UserAuthorization> GetUserAuthorizations(IEnumerable<string>? agentIds = null)
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(_user.Id);
var (isAdmin, user) = await IsAdminUser(_user.Id);
var auth = new UserAuthorization();
if (user == null) return auth;
auth.IsAdmin = UserConstant.AdminRoles.Contains(user.Role);
auth.IsAdmin = isAdmin;
var role = db.GetRoles(new RoleFilter { Names = [user.Role] }).FirstOrDefault();
var permissions = user.Permissions?.Any() == true ? user.Permissions : role?.Permissions ?? [];
auth.Permissions = permissions;

View file

@ -1,10 +1,8 @@
using Azure;
using BotSharp.Abstraction.Files.Constants;
using BotSharp.Abstraction.Files.Enums;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Options;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Users.Enums;
using BotSharp.Core.Infrastructures;
namespace BotSharp.OpenAPI.Controllers;
@ -45,27 +43,33 @@ public class ConversationController : ControllerBase
return ConversationViewModel.FromSession(conv);
}
[HttpPost("/conversations")]
public async Task<PagedItems<ConversationViewModel>> GetConversations([FromBody] ConversationFilter filter)
[HttpGet("/conversations")]
public async Task<PagedItems<ConversationViewModel>> GetConversations([FromQuery] ConversationFilter filter)
{
var convService = _services.GetRequiredService<IConversationService>();
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
if (user == null)
{
return new PagedItems<ConversationViewModel>();
}
filter.UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : filter.UserId;
filter.UserId = !isAdmin ? user.Id : filter.UserId;
var conversations = await convService.GetConversations(filter);
var agentService = _services.GetRequiredService<IAgentService>();
var list = conversations.Items.Select(x => ConversationViewModel.FromSession(x)).ToList();
var agentIds = list.Select(x => x.AgentId).ToList();
var agents = await agentService.GetAgentOptions(agentIds);
var userIds = list.Select(x => x.User.Id).ToList();
var users = await userService.GetUsers(new UserFilter { UserIds = userIds, Size = filter.Pager.Size });
foreach (var item in list)
{
user = await userService.GetUser(item.User.Id);
user = users.Items.FirstOrDefault(x => x.Id == item.User.Id);
item.User = UserViewModel.FromUser(user);
var agent = await agentService.GetAgent(item.AgentId);
var agent = agents.FirstOrDefault(x => x.Id == item.AgentId);
item.AgentName = agent?.Name ?? "Unkown";
}
@ -138,7 +142,7 @@ public class ConversationController : ControllerBase
{
var service = _services.GetRequiredService<IConversationService>();
var userService = _services.GetRequiredService<IUserService>();
var user = await userService.GetUser(_user.Id);
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
if (user == null)
{
return null;
@ -147,7 +151,7 @@ public class ConversationController : ControllerBase
var filter = new ConversationFilter
{
Id = conversationId,
UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : null
UserId = !isAdmin ? user.Id : null
};
var conversations = await service.GetConversations(filter);
if (conversations.Items.IsNullOrEmpty())
@ -206,11 +210,11 @@ public class ConversationController : ControllerBase
var userService = _services.GetRequiredService<IUserService>();
var conv = _services.GetRequiredService<IConversationService>();
var user = await userService.GetUser(_user.Id);
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
var filter = new ConversationFilter
{
Id = conversationId,
UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : null
UserId = !isAdmin ? user?.Id : null
};
var conversations = await conv.GetConversations(filter);
@ -229,11 +233,11 @@ public class ConversationController : ControllerBase
var userService = _services.GetRequiredService<IUserService>();
var conversationService = _services.GetRequiredService<IConversationService>();
var user = await userService.GetUser(_user.Id);
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
var filter = new ConversationFilter
{
Id = conversationId,
UserId = user.Role != UserRole.Admin ? user.Id : null
UserId = !isAdmin ? user?.Id : null
};
var conversations = await conversationService.GetConversations(filter);
@ -283,11 +287,11 @@ public class ConversationController : ControllerBase
var userService = _services.GetRequiredService<IUserService>();
var conversationService = _services.GetRequiredService<IConversationService>();
var user = await userService.GetUser(_user.Id);
var (isAdmin, user) = await userService.IsAdminUser(_user.Id);
var filter = new ConversationFilter
{
Id = conversationId,
UserId = !UserConstant.AdminRoles.Contains(user?.Role) ? user.Id : null
UserId = !isAdmin ? user?.Id : null
};
var conversations = await conversationService.GetConversations(filter);
@ -555,10 +559,10 @@ public class ConversationController : ControllerBase
#region Search state keys
[HttpGet("/conversation/state/keys")]
public async Task<List<string>> GetConversationStateKeys([FromQuery] string query, [FromQuery] int keyLimit = 10, [FromQuery] int convLimit = 100, [FromQuery] bool preload = false)
public async Task<List<string>> GetConversationStateKeys([FromQuery] ConversationStateKeysFilter request)
{
var convService = _services.GetRequiredService<IConversationService>();
var keys = await convService.GetConversationStateSearhKeys(query, keyLimit: keyLimit, convLimit: convLimit, preload: preload);
var keys = await convService.GetConversationStateSearhKeys(request);
return keys;
}
#endregion

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Loggers.Services;
using BotSharp.OpenAPI.ViewModels.Instructs;
@ -39,6 +38,7 @@ public class LoggerController : ControllerBase
}
}
#region Conversation log
[HttpGet("/logger/conversation/{conversationId}/content-log")]
public async Task<List<ContentLogOutputModel>> GetConversationContentLogs([FromRoute] string conversationId)
{
@ -52,7 +52,9 @@ public class LoggerController : ControllerBase
var logging = _services.GetRequiredService<ILoggerService>();
return await logging.GetConversationStateLogs(conversationId);
}
#endregion
#region Instruction log
[HttpGet("/logger/instruction/log")]
public async Task<PagedItems<InstructionLogViewModel>> GetInstructionLogs([FromQuery] InstructLogFilter request)
{
@ -65,4 +67,13 @@ public class LoggerController : ControllerBase
Count = logs.Count
};
}
[HttpGet("/logger/instruction/log/keys")]
public async Task<List<string>> GetInstructionLogKeys([FromQuery] InstructLogKeysFilter request)
{
var logging = _services.GetRequiredService<ILoggerService>();
var keys = await logging.GetInstructionLogSearchKeys(request);
return keys;
}
#endregion
}

View file

@ -89,6 +89,7 @@ public class PluginController(IServiceProvider services, IUserIdentity user, Plu
private async Task<bool> IsValidUser()
{
var userService = services.GetRequiredService<IUserService>();
return await userService.IsAdminUser(_user.Id);
var (isAdmin, _) = await userService.IsAdminUser(_user.Id);
return isAdmin;
}
}

View file

@ -1,5 +1,4 @@
using BotSharp.Abstraction.Roles;
using BotSharp.Abstraction.Users.Enums;
namespace BotSharp.OpenAPI.Controllers;
@ -83,6 +82,7 @@ public class RoleController : ControllerBase
private async Task<bool> IsValidUser()
{
var userService = _services.GetRequiredService<IUserService>();
return await userService.IsAdminUser(_user.Id);
var (isAdmin, _) = await userService.IsAdminUser(_user.Id);
return isAdmin;
}
}

View file

@ -254,7 +254,8 @@ public class UserController : ControllerBase
private async Task<bool> IsValidUser()
{
var userService = _services.GetRequiredService<IUserService>();
return await userService.IsAdminUser(_user.Id);
var (isAdmin, _) = await userService.IsAdminUser(_user.Id);
return isAdmin;
}
private FileContentResult BuildFileResult(string file)

View file

@ -644,21 +644,31 @@ public partial class MongoRepository
#if !DEBUG
[SharpCache(10)]
#endif
public List<string> GetConversationStateSearchKeys(int messageLowerLimit = 2, int convUpperLimit = 100)
public List<string> GetConversationStateSearchKeys(ConversationStateKeysFilter filter)
{
var stateBuilder = Builders<ConversationStateDocument>.Filter;
var sortDef = Builders<ConversationStateDocument>.Sort.Descending(x => x.UpdatedTime);
var stateFilters = new List<FilterDefinition<ConversationStateDocument>>()
var builder = Builders<ConversationDocument>.Filter;
var sortDef = Builders<ConversationDocument>.Sort.Descending(x => x.UpdatedTime);
var filters = new List<FilterDefinition<ConversationDocument>>()
{
stateBuilder.Exists(x => x.States),
stateBuilder.Ne(x => x.States, [])
builder.Exists(x => x.LatestStates),
builder.Ne(x => x.LatestStates, [])
};
var states = _dc.ConversationStates.Find(stateBuilder.And(stateFilters))
.Sort(sortDef)
.Limit(convUpperLimit)
.ToList();
var keys = states.SelectMany(x => x.States.Select(x => x.Key)).Distinct().ToList();
if (!filter.AgentIds.IsNullOrEmpty())
{
filters.Add(builder.In(x => x.AgentId, filter.AgentIds));
}
if (!filter.UserIds.IsNullOrEmpty())
{
filters.Add(builder.In(x => x.UserId, filter.UserIds));
}
var convDocs = _dc.Conversations.Find(builder.And(filters))
.Sort(sortDef)
.Limit(filter.ConvLimit)
.ToList();
var keys = convDocs.SelectMany(x => x.LatestStates.Select(x => x.Key)).Distinct().ToList();
return keys;
}

View file

@ -1,4 +1,3 @@
using BotSharp.Abstraction.Instructs.Models;
using BotSharp.Abstraction.Loggers.Models;
using BotSharp.Abstraction.Repositories.Filters;
using System.Text.Json;
@ -241,5 +240,33 @@ public partial class MongoRepository
Count = (int)count
};
}
public List<string> GetInstructionLogSearchKeys(InstructLogKeysFilter filter)
{
var builder = Builders<InstructionLogDocument>.Filter;
var sortDef = Builders<InstructionLogDocument>.Sort.Descending(x => x.CreatedTime);
var filters = new List<FilterDefinition<InstructionLogDocument>>()
{
builder.Exists(x => x.States),
builder.Ne(x => x.States, [])
};
if (!filter.AgentIds.IsNullOrEmpty())
{
filters.Add(builder.In(x => x.AgentId, filter.AgentIds));
}
if (!filter.UserIds.IsNullOrEmpty())
{
filters.Add(builder.In(x => x.UserId, filter.UserIds));
}
var convDocs = _dc.InstructionLogs.Find(builder.And(filters))
.Sort(sortDef)
.Limit(filter.LogLimit)
.ToList();
var keys = convDocs.SelectMany(x => x.States.Select(x => x.Key)).Distinct().ToList();
return keys;
}
#endregion
}